diff --git a/.eslintignore b/.eslintignore
index 63cd01d6e90db..f757ed9a1bf98 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -30,8 +30,6 @@ snapshots.js
# package overrides
/packages/elastic-eslint-config-kibana
-/packages/kbn-interpreter/src/common/lib/grammar.js
-/packages/kbn-tinymath/src/grammar.js
/packages/kbn-plugin-generator/template
/packages/kbn-pm/dist
/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/
diff --git a/.eslintrc.js b/.eslintrc.js
index 40dd6a55a2a3f..c64f03a8398e5 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -893,6 +893,8 @@ module.exports = {
files: [
'x-pack/plugins/security_solution/public/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/security_solution/common/**/*.{js,mjs,ts,tsx}',
+ 'x-pack/plugins/timelines/public/**/*.{js,mjs,ts,tsx}',
+ 'x-pack/plugins/timelines/common/**/*.{js,mjs,ts,tsx}',
],
rules: {
'import/no-nodejs-modules': 'error',
@@ -907,7 +909,10 @@ module.exports = {
},
{
// typescript only for front and back end
- files: ['x-pack/plugins/security_solution/**/*.{ts,tsx}'],
+ files: [
+ 'x-pack/plugins/security_solution/**/*.{ts,tsx}',
+ 'x-pack/plugins/timelines/**/*.{ts,tsx}',
+ ],
rules: {
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/no-explicit-any': 'error',
@@ -917,7 +922,10 @@ module.exports = {
},
{
// typescript and javascript for front and back end
- files: ['x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}'],
+ files: [
+ 'x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}',
+ 'x-pack/plugins/timelines/**/*.{js,mjs,ts,tsx}',
+ ],
plugins: ['eslint-plugin-node', 'react'],
env: {
jest: true,
diff --git a/docs/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc
index 267ab3891d700..5bd3a7587dde9 100644
--- a/docs/api/saved-objects/bulk_create.asciidoc
+++ b/docs/api/saved-objects/bulk_create.asciidoc
@@ -45,6 +45,11 @@ experimental[] Create multiple {kib} saved objects.
(Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the
object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space
(default behavior).
+* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including
+the "All spaces" identifier (`'*'`).
+* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be
+used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
`version`::
(Optional, number) Specifies the version.
diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc
index d7a368034ef07..e7e25c7d3bba6 100644
--- a/docs/api/saved-objects/create.asciidoc
+++ b/docs/api/saved-objects/create.asciidoc
@@ -52,6 +52,11 @@ any data that you send to the API is properly formed.
(Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the
object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space
(default behavior).
+* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including
+the "All spaces" identifier (`'*'`).
+* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be
+used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+* For global object types (registered with `namespaceType: 'agnostic'): this option cannot be used.
[[saved-objects-api-create-request-codes]]
==== Response code
diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc
index c211751c09b49..5d7ba22841aa1 100644
--- a/docs/developer/getting-started/monorepo-packages.asciidoc
+++ b/docs/developer/getting-started/monorepo-packages.asciidoc
@@ -80,11 +80,13 @@ yarn kbn watch-bazel
- @kbn/eslint-plugin-eslint
- @kbn/expect
- @kbn/i18n
+- @kbn/interpreter
- @kbn/io-ts-utils
- @kbn/legacy-logging
- @kbn/logging
- @kbn/mapbox-gl
- @kbn/monaco
+- @kbn/optimizer
- @kbn/rule-data-utils
- @kbn/securitysolution-es-utils
- @kbn/securitysolution-hook-utils
@@ -100,8 +102,10 @@ yarn kbn watch-bazel
- @kbn/server-http-tools
- @kbn/server-route-repository
- @kbn/std
+- @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 9930ab7319f65..b10ad949c4944 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
@@ -106,6 +106,7 @@ readonly links: {
};
readonly search: {
readonly sessions: string;
+ readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
@@ -116,6 +117,7 @@ readonly links: {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
@@ -185,5 +187,18 @@ readonly links: {
readonly plugins: Record;
readonly snapshotRestore: Record;
readonly ingest: Record;
+ 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;
+ }>;
};
```
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 ab8cdea5e4d86..c020f57faa882 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
@@ -17,5 +17,5 @@ export interface DocLinksStart
| --- | --- | --- |
| [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string
| |
| [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string
| |
-| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
}
| |
+| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
}
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
index 3db8bbadfbd6b..4d094ecde7a96 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
@@ -6,7 +6,7 @@
Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).
-Note: this can only be used for multi-namespace object types.
+\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
index 6fc01212a2e41..463c3fe81b702 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
@@ -18,7 +18,7 @@ export interface SavedObjectsBulkCreateObject
| [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T
| |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
| [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string
| |
-| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. |
+| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'
): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'
). \* For isolated object types (registered with namespaceType: 'single'
or namespaceType: 'multiple-isolated'
): this option can only be used to specify a single space, and the "All spaces" identifier ('*'
) is not allowed. \* For global object types (registered with namespaceType: 'agnostic'
): this option cannot be used. |
| [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion
| Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [originId](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) | string
| Optional ID of the original saved object, if this object's id
was regenerated |
| [references](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[]
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
index 262b0997cb905..43489b8d2e8a2 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
@@ -6,7 +6,7 @@
Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).
-Note: this can only be used for multi-namespace object types.
+\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
index 1805f389d4e7f..7eaa9c51f5c82 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
@@ -17,7 +17,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions
| --- | --- | --- |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
| [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string
| (not recommended) Specify an id for the document |
-| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. |
+| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'
): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'
). \* For isolated object types (registered with namespaceType: 'single'
or namespaceType: 'multiple-isolated'
): this option can only be used to specify a single space, and the "All spaces" identifier ('*'
) is not allowed. \* For global object types (registered with namespaceType: 'agnostic'
): this option cannot be used. |
| [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion
| Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [originId](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) | string
| Optional ID of the original saved object, if this object's id
was regenerated |
| [overwrite](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | boolean
| Overwrite existing documents (defaults to false) |
diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md
index 143cd397c40ae..bf08ca1682f3b 100644
--- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md
+++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md
@@ -24,5 +24,7 @@ set(status$: Observable): void;
## Remarks
+The first emission from this Observable should occur within 30s, else this plugin's status will fallback to `unavailable` until the first emission.
+
See the [StatusServiceSetup.derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) API for leveraging the default status calculation that is provided by Core.
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md
new file mode 100644
index 0000000000000..2af44037292a2
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [getResolvedTimeRange](./kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md)
+
+## AggConfigs.getResolvedTimeRange() method
+
+Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set)
+
+Signature:
+
+```typescript
+getResolvedTimeRange(): import("../..").TimeRangeBounds | undefined;
+```
+Returns:
+
+`import("../..").TimeRangeBounds | undefined`
+
+Current time range as resolved date.
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md
index 45333b6767cac..9e671675b0b29 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md
@@ -42,6 +42,7 @@ export declare class AggConfigs
| [getAll()](./kibana-plugin-plugins-data-public.aggconfigs.getall.md) | | |
| [getRequestAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggbyid.md) | | |
| [getRequestAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggs.md) | | |
+| [getResolvedTimeRange()](./kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md) | | Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) |
| [getResponseAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggbyid.md) | | Find a response agg by it's id. This may be an agg in the aggConfigs, or one created specifically for a response value |
| [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md) | | Gets the AggConfigs (and possibly ResponseAggConfigs) that represent the values that will be produced when all aggs are run.With multi-value metric aggs it is possible for a single agg request to result in multiple agg values, which is why the length of a vis' responseValuesAggs may be different than the vis' aggs {array\[AggConfig\]} |
| [getSearchSourceTimeFilter(forceNow)](./kibana-plugin-plugins-data-public.aggconfigs.getsearchsourcetimefilter.md) | | |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md
index 54b5a33ccf682..2ca4847d6dc39 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md
@@ -13,11 +13,11 @@ esFilters: {
FILTERS: typeof FILTERS;
FilterStateStore: typeof FilterStateStore;
buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter;
- buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter;
- buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter;
- buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter;
+ buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhrasesFilter;
+ buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").MinimalIndexPattern) => import("../common").ExistsFilter;
+ buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhraseFilter;
buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter;
- buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
+ buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").MinimalIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
isPhraseFilter: (filter: any) => filter is import("../common").PhraseFilter;
isExistsFilter: (filter: any) => filter is import("../common").ExistsFilter;
isPhrasesFilter: (filter: any) => filter is import("../common").PhrasesFilter;
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md
index 2cde2b7455585..881a1fa803ca6 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md
@@ -10,6 +10,6 @@
esKuery: {
nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes;
fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode;
- toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
+ toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").MinimalIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
}
```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md
index 2430e6a93bd2b..70805aaaaee8c 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md
@@ -10,7 +10,7 @@
esQuery: {
buildEsQuery: typeof buildEsQuery;
getEsQueryConfig: typeof getEsQueryConfig;
- buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
+ buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").MinimalIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
must: never[];
filter: import("../common").Filter[];
should: never[];
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fields.md
deleted file mode 100644
index 792bee44f96a8..0000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fields.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md)
-
-## IIndexPattern.fields property
-
-Signature:
-
-```typescript
-fields: IFieldType[];
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.id.md
deleted file mode 100644
index 917a80975df6c..0000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.id.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md)
-
-## IIndexPattern.id property
-
-Signature:
-
-```typescript
-id?: string;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
index bf7f88ab37039..88d8520a373c6 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
@@ -12,7 +12,7 @@
Signature:
```typescript
-export interface IIndexPattern
+export interface IIndexPattern extends MinimalIndexPattern
```
## Properties
@@ -20,9 +20,7 @@ export interface IIndexPattern
| Property | Type | Description |
| --- | --- | --- |
| [fieldFormatMap](./kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md) | Record<string, SerializedFieldFormat<unknown> | undefined>
| |
-| [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md) | IFieldType[]
| |
| [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) | (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat
| Look up a formatter for a given field |
-| [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md) | string
| |
| [timeFieldName](./kibana-plugin-plugins-data-public.iindexpattern.timefieldname.md) | string
| |
| [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string
| |
| [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string
| Type is used for identifying rollup indices, otherwise left undefined |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md
new file mode 100644
index 0000000000000..d649212ae0547
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) > [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md)
+
+## IKibanaSearchResponse.isRestored property
+
+Indicates whether the results returned are from the async-search index
+
+Signature:
+
+```typescript
+isRestored?: boolean;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md
index 1d3e0c08dfc18..c7046902dac72 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md
@@ -16,6 +16,7 @@ export interface IKibanaSearchResponse
| --- | --- | --- |
| [id](./kibana-plugin-plugins-data-public.ikibanasearchresponse.id.md) | string
| Some responses may contain a unique id to identify the request this response came from. |
| [isPartial](./kibana-plugin-plugins-data-public.ikibanasearchresponse.ispartial.md) | boolean
| Indicates whether the results returned are complete or partial |
+| [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md) | boolean
| Indicates whether the results returned are from the async-search index |
| [isRunning](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrunning.md) | boolean
| Indicates whether search is still in flight |
| [loaded](./kibana-plugin-plugins-data-public.ikibanasearchresponse.loaded.md) | number
| If relevant to the search strategy, return a loaded number that represents how progress is indicated. |
| [rawResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md) | RawResponse
| The raw response returned by the internal search method (usually the raw ES response) |
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md
index d7e80d94db4e6..d951cb2426943 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md
@@ -11,11 +11,11 @@ esFilters: {
buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter;
buildCustomFilter: typeof buildCustomFilter;
buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter;
- buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter;
+ buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").MinimalIndexPattern) => import("../common").ExistsFilter;
buildFilter: typeof buildFilter;
- buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter;
- buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter;
- buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
+ buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhraseFilter;
+ buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhrasesFilter;
+ buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").MinimalIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
isFilterDisabled: (filter: import("../common").Filter) => boolean;
}
```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md
index 4b96d8af756f3..6274eb5f4f4a5 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md
@@ -10,6 +10,6 @@
esKuery: {
nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes;
fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode;
- toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
+ toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").MinimalIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
}
```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md
index ac9be23bc6b6f..0d1baecb014f5 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md
@@ -8,7 +8,7 @@
```typescript
esQuery: {
- buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
+ buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").MinimalIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
must: never[];
filter: import("../common").Filter[];
should: never[];
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md
index b1745b298e27e..9816b884c4614 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md
@@ -13,6 +13,7 @@
| [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | |
| [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | |
| [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) | |
+| [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) | |
| [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | |
| [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | |
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md
new file mode 100644
index 0000000000000..e48a1c98f8578
--- /dev/null
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) > [(constructor)](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md)
+
+## NoSearchIdInSessionError.(constructor)
+
+Constructs a new instance of the `NoSearchIdInSessionError` class
+
+Signature:
+
+```typescript
+constructor();
+```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md
new file mode 100644
index 0000000000000..707739f845cd1
--- /dev/null
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md
@@ -0,0 +1,18 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md)
+
+## NoSearchIdInSessionError class
+
+Signature:
+
+```typescript
+export declare class NoSearchIdInSessionError extends KbnError
+```
+
+## Constructors
+
+| Constructor | Modifiers | Description |
+| --- | --- | --- |
+| [(constructor)()](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md) | | Constructs a new instance of the NoSearchIdInSessionError
class |
+
diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc
index 65b600d4b7281..3d3d7aeb2d777 100644
--- a/docs/management/action-types.asciidoc
+++ b/docs/management/action-types.asciidoc
@@ -43,6 +43,10 @@ a| <>
| Send a message to a Slack channel or user.
+a| <>
+
+| Create an incident in Swimlane.
+
a| <>
| Send a request to a web service.
diff --git a/docs/management/connectors/action-types/swimlane.asciidoc b/docs/management/connectors/action-types/swimlane.asciidoc
new file mode 100644
index 0000000000000..88447bb496a86
--- /dev/null
+++ b/docs/management/connectors/action-types/swimlane.asciidoc
@@ -0,0 +1,105 @@
+[role="xpack"]
+[[swimlane-action-type]]
+=== Swimlane connector and action
+++++
+Swimlane
+++++
+
+The Swimlane connector uses the https://swimlane.com/knowledge-center/docs/developer-guide/rest-api/[Swimlane REST API] to create Swimlane records.
+
+[float]
+[[swimlane-connector-configuration]]
+==== Connector configuration
+
+Swimlane connectors have the following configuration properties.
+
+Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action.
+URL:: Swimlane instance URL.
+Application ID:: Swimlane application ID.
+API token:: Swimlane API authentication token for HTTP Basic authentication.
+
+[float]
+[[Preconfigured-swimlane-configuration]]
+==== Preconfigured connector type
+
+[source,text]
+--
+ my-swimlane:
+ name: preconfigured-swimlane-connector-type
+ actionTypeId: .swimlane
+ config:
+ apiUrl: https://elastic.swimlaneurl.us
+ appId: app-id
+ mappings:
+ alertIdConfig:
+ fieldType: text
+ id: agp4s
+ key: alert-id
+ name: Alert ID
+ caseIdConfig:
+ fieldType: text
+ id: ae1mi
+ key: case-id
+ name: Case ID
+ caseNameConfig:
+ fieldType: text
+ id: anxnr
+ key: case-name
+ name: Case Name
+ commentsConfig:
+ fieldType: comments
+ id: au18d
+ key: comments
+ name: Comments
+ descriptionConfig:
+ fieldType: text
+ id: ae1gd
+ key: description
+ name: Description
+ ruleNameConfig:
+ fieldType: text
+ id: avfsl
+ key: rule-name
+ name: Rule Name
+ severityConfig:
+ fieldType: text
+ id: a71ik
+ key: severity
+ name: severity
+ secrets:
+ apiToken: tokenkeystorevalue
+--
+
+Config defines information for the connector type.
+
+`apiUrl`:: An address that corresponds to *URL*.
+`appId`:: A key that corresponds to *Application ID*.
+
+Secrets defines sensitive information for the connector type.
+
+`apiToken`:: A string that corresponds to *API Token*. Should be stored in the <>.
+
+[float]
+[[define-swimlane-ui]]
+==== Define connector in Stack Management
+
+Define Swimlane connector properties.
+
+[role="screenshot"]
+image::management/connectors/images/swimlane-connector.png[Swimlane connector]
+
+Test Swimlane action parameters.
+
+[role="screenshot"]
+image::management/connectors/images/swimlane-params-test.png[Swimlane params test]
+
+[float]
+[[swimlane-action-configuration]]
+==== Action configuration
+
+Swimlane actions have the following configuration properties.
+
+Comments:: Additional information for the client, such as how to troubleshoot the issue.
+Severity:: The severity of the incident.
+
+NOTE: Alert ID and Rule Name are filled automatically. Specifically, Alert ID is set to `{{alert.id}}` and Rule Name to `{{rule.name}}`.
\ No newline at end of file
diff --git a/docs/management/connectors/images/swimlane-connector.png b/docs/management/connectors/images/swimlane-connector.png
new file mode 100644
index 0000000000000..520c35d00381b
Binary files /dev/null and b/docs/management/connectors/images/swimlane-connector.png differ
diff --git a/docs/management/connectors/images/swimlane-params-test.png b/docs/management/connectors/images/swimlane-params-test.png
new file mode 100644
index 0000000000000..c0e02c2c7b18f
Binary files /dev/null and b/docs/management/connectors/images/swimlane-params-test.png differ
diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc
index ea4fa46d3e808..033b1c3ac150e 100644
--- a/docs/management/connectors/index.asciidoc
+++ b/docs/management/connectors/index.asciidoc
@@ -6,6 +6,7 @@ include::action-types/teams.asciidoc[]
include::action-types/pagerduty.asciidoc[]
include::action-types/server-log.asciidoc[]
include::action-types/servicenow.asciidoc[]
+include::action-types/swimlane.asciidoc[]
include::action-types/slack.asciidoc[]
include::action-types/webhook.asciidoc[]
include::pre-configured-connectors.asciidoc[]
diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc
index 71f141d1ed5d6..d1d283ca60fbb 100644
--- a/docs/settings/alert-action-settings.asciidoc
+++ b/docs/settings/alert-action-settings.asciidoc
@@ -69,7 +69,7 @@ You can configure the following settings in the `kibana.yml` file.
--
xpack.actions.customHostSettings:
- url: smtp://mail.example.com:465
- tls:
+ ssl:
verificationMode: 'full'
certificateAuthoritiesFiles: [ 'one.crt' ]
certificateAuthoritiesData: |
@@ -79,7 +79,7 @@ xpack.actions.customHostSettings:
smtp:
requireTLS: true
- url: https://webhook.example.com
- tls:
+ ssl:
// legacy
rejectUnauthorized: false
verificationMode: 'none'
@@ -97,8 +97,8 @@ xpack.actions.customHostSettings:
server, and the `https` URLs are used for actions which use `https` to
connect to services. +
+
- Entries with `https` URLs can use the `tls` options, and entries with `smtp`
- URLs can use both the `tls` and `smtp` options. +
+ Entries with `https` URLs can use the `ssl` options, and entries with `smtp`
+ URLs can use both the `ssl` and `smtp` options. +
+
No other URL values should be part of this URL, including paths,
query strings, and authentication information. When an http or smtp request
@@ -117,24 +117,24 @@ xpack.actions.customHostSettings:
The options `smtp.ignoreTLS` and `smtp.requireTLS` can not both be set to true.
| `xpack.actions.customHostSettings[n]`
-`.tls.rejectUnauthorized` {ess-icon}
- | Deprecated. Use <> instead. A boolean value indicating whether to bypass server certificate validation.
+`.ssl.rejectUnauthorized` {ess-icon}
+ | Deprecated. Use <> instead. A boolean value indicating whether to bypass server certificate validation.
Overrides the general `xpack.actions.rejectUnauthorized` configuration
for requests made for this hostname/port.
|[[action-config-custom-host-verification-mode]] `xpack.actions.customHostSettings[n]`
-`.tls.verificationMode`
+`.ssl.verificationMode`
| Controls the verification of the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to the host server. Valid values are `full`, `certificate`, and `none`.
- Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. Overrides the general `xpack.actions.tls.verificationMode` configuration
+ Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. Overrides the general `xpack.actions.ssl.verificationMode` configuration
for requests made for this hostname/port.
| `xpack.actions.customHostSettings[n]`
-`.tls.certificateAuthoritiesFiles`
+`.ssl.certificateAuthoritiesFiles`
| A file name or list of file names of PEM-encoded certificate files to use
to validate the server.
| `xpack.actions.customHostSettings[n]`
-`.tls.certificateAuthoritiesData` {ess-icon}
+`.ssl.certificateAuthoritiesData` {ess-icon}
| The contents of a PEM-encoded certificate file, or multiple files appended
into a single string. This configuration can be used for environments where
the files cannot be made available.
@@ -165,28 +165,28 @@ xpack.actions.customHostSettings:
a|`xpack.actions.`
`proxyRejectUnauthorizedCertificates` {ess-icon}
- | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`.
+ | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`.
|[[action-config-proxy-verification-mode]]
`xpack.actions[n]`
-`.tls.proxyVerificationMode` {ess-icon}
+`.ssl.proxyVerificationMode` {ess-icon}
| Controls the verification for the proxy server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to the proxy server. Valid values are `full`, `certificate`, and `none`.
Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>.
| `xpack.actions.rejectUnauthorized` {ess-icon}
- | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. +
+ | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. +
+
As an alternative to setting `xpack.actions.rejectUnauthorized`, you can use the setting
- `xpack.actions.customHostSettings` to set TLS options for specific servers.
+ `xpack.actions.customHostSettings` to set SSL options for specific servers.
|[[action-config-verification-mode]]
`xpack.actions[n]`
-`.tls.verificationMode` {ess-icon}
+`.ssl.verificationMode` {ess-icon}
| Controls the verification for the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection for actions. Valid values are `full`, `certificate`, and `none`.
Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. +
+
- As an alternative to setting `xpack.actions.tls.verificationMode`, you can use the setting
- `xpack.actions.customHostSettings` to set TLS options for specific servers.
+ As an alternative to setting `xpack.actions.ssl.verificationMode`, you can use the setting
+ `xpack.actions.customHostSettings` to set SSL options for specific servers.
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index ddb906f390a2d..c3c29adcea18f 100644
--- a/docs/setup/settings.asciidoc
+++ b/docs/setup/settings.asciidoc
@@ -325,6 +325,9 @@ The time interval policy will rotate the log file every given interval of time.
When `includeElasticMapsService` is turned off, only the vector layers configured by <>
and the tile layer configured by <> are available in <>. *Default: `true`*
+| `map.emsUrl:`
+ | Specifies the URL of a self hosted <>
+
| `map.proxyElasticMapsServiceInMaps:`
| Set to `true` to proxy all <> Elastic Maps Service
requests through the {kib} server. *Default: `false`*
diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc
index 8c17f8ec93b96..b699c56ebd944 100644
--- a/docs/user/alerting/alerting-getting-started.asciidoc
+++ b/docs/user/alerting/alerting-getting-started.asciidoc
@@ -136,9 +136,4 @@ Functionally, {kib} alerting differs in that:
At a higher level, {kib} alerting allows rich integrations across use cases like <>, <>, <>, and <>.
Pre-packaged *rule types* simplify setup and hide the details of complex, domain-specific detections, while providing a consistent interface across {kib}.
-[float]
-[[alerting-setup-prerequisites]]
-== Prerequisites
-<>
-
--
\ No newline at end of file
diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc
index 39f1af0030e0a..2ae5160069f0a 100644
--- a/docs/user/alerting/alerting-setup.asciidoc
+++ b/docs/user/alerting/alerting-setup.asciidoc
@@ -1,8 +1,8 @@
[role="xpack"]
[[alerting-setup]]
-== Alerting Setup
+== Alerting Set up
++++
-Setup
+Set up
++++
The Alerting feature is automatically enabled in {kib}, but might require some additional configuration.
diff --git a/docs/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc
index b7b0c749dfe14..08655508b3cba 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/alerting/defining-rules.asciidoc b/docs/user/alerting/defining-rules.asciidoc
deleted file mode 100644
index 686a7bbc8a37b..0000000000000
--- a/docs/user/alerting/defining-rules.asciidoc
+++ /dev/null
@@ -1,11 +0,0 @@
-[role="xpack"]
-[[defining-alerts]]
-== Defining rules
-
-This content has been moved to <>.
-
-[float]
-[[defining-alerts-general-details]]
-==== General rule details
-
-This content has been moved to <>.
\ No newline at end of file
diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc
index 9ab6a2dc46ebf..957d99a54ebaa 100644
--- a/docs/user/alerting/index.asciidoc
+++ b/docs/user/alerting/index.asciidoc
@@ -1,7 +1,5 @@
include::alerting-getting-started.asciidoc[]
include::alerting-setup.asciidoc[]
include::create-and-manage-rules.asciidoc[]
-include::defining-rules.asciidoc[]
-include::rule-management.asciidoc[]
include::rule-types.asciidoc[]
include::alerting-troubleshooting.asciidoc[]
diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc
deleted file mode 100644
index d6349a60e08eb..0000000000000
--- a/docs/user/alerting/rule-management.asciidoc
+++ /dev/null
@@ -1,5 +0,0 @@
-[role="xpack"]
-[[alert-management]]
-== Managing rules
-
-This content has been moved to <>.
\ No newline at end of file
diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc
index bb840014fe80f..f7f57d2f845a0 100644
--- a/docs/user/alerting/rule-types.asciidoc
+++ b/docs/user/alerting/rule-types.asciidoc
@@ -15,7 +15,7 @@ see {subscriptions}[the subscription page].
[[stack-rules]]
=== Stack rules
-<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information.
+<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information.
[cols="2*<"]
|===
diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc
index cb5c484def3b9..17bfc19c2e0c9 100644
--- a/docs/user/dashboard/aggregation-reference.asciidoc
+++ b/docs/user/dashboard/aggregation-reference.asciidoc
@@ -12,91 +12,168 @@ This reference can help simplify the comparison if you need a specific feature.
[options="header"]
|===
-| Type | Aggregation-based | Lens | TSVB | Timelion | Vega
+| Type | Lens | TSVB | Agg-based | Vega | Timelion
| Table
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
|
|
-| Table with summary row
-^| X
-^| X
-|
+| Bar, line, and area
+| ✓
+| ✓
+| ✓
+| ✓
+| ✓
+
+| Split chart/small multiples
|
+| ✓
+| ✓
+| ✓
|
-| Bar, line, and area charts
-^| X
-^| X
-^| X
-^| X
-^| X
+| Pie and donut
+| ✓
+|
+| ✓
+| ✓
+|
-| Percentage bar or area chart
+| Sunburst
+| ✓
|
-^| X
-^| X
+| ✓
+| ✓
|
-^| X
-| Split bar, line, and area charts
-^| X
+| Treemap
+| ✓
+|
|
+| ✓
|
+
+| Heat map
+| ✓
+| ✓
+| ✓
+| ✓
|
-^| X
-| Pie and donut charts
-^| X
-^| X
+| Gauge and Goal
|
+| ✓
+| ✓
+| ✓
|
-^| X
-| Sunburst chart
-^| X
-^| X
+| Markdown
+|
+| ✓
|
|
|
-| Heat map
-^| X
-^| X
+| Metric
+| ✓
+| ✓
+| ✓
+| ✓
+|
+
+| Tag cloud
|
|
-^| X
+| ✓
+| ✓
+|
-| Gauge and Goal
-^| X
+|===
+
+[float]
+[[table-features]]
+=== Table features
+
+[options="header"]
+|===
+
+| Type | Lens | TSVB | Agg-based
+
+| Summary row
+| ✓
|
-^| X
+| ✓
+
+| Pivot table
+| ✓
|
|
-| Markdown
+| Calculated column
+| Formula
+| ✓
+| Percent only
+
+| Color by value
+| ✓
+| ✓
|
+
+|===
+
+[float]
+[[xy-features]]
+=== Bar, line, area features
+
+[options="header"]
+|===
+
+| Type | Lens | TSVB | Agg-based | Vega | Timelion
+
+| Dense time series
+| Customizable
+| ✓
+| Customizable
+| ✓
+| ✓
+
+| Percentage mode
+| ✓
+| ✓
+| ✓
+| ✓
|
-^| X
+
+| Break downs
+| 1
+| 1
+| 3
+| ∞
+| 1
+
+| Custom color with break downs
|
+| Only for Filters
+| ✓
+| ✓
|
-| Metric
-^| X
-^| X
-^| X
+| Fit missing values
+| ✓
|
-^| X
+| ✓
+| ✓
+| ✓
-| Tag cloud
-^| X
+| Synchronized tooltips
+|
+| ✓
|
|
|
-^| X
|===
@@ -111,67 +188,57 @@ For information about {es} bucket aggregations, refer to {ref}/search-aggregatio
[options="header"]
|===
-| Type | Agg-based | Markdown | Lens | TSVB
+| Type | Lens | TSVB | Agg-based
| Histogram
-^| X
-^| X
-^| X
+| ✓
|
+| ✓
| Date histogram
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Date range
-^| X
-^| X
-|
+| Use filters
|
+| ✓
| Filter
-^| X
-^| X
|
-^| X
+| ✓
+|
| Filters
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| GeoHash grid
-^| X
-^| X
|
|
+| ✓
| IP range
-^| X
-^| X
-|
-|
+| Use filters
+| Use filters
+| ✓
| Range
-^| X
-^| X
-^| X
-|
+| ✓
+| Use filters
+| ✓
| Terms
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Significant terms
-^| X
-^| X
|
-^| X
+|
+| ✓
|===
@@ -186,67 +253,57 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati
[options="header"]
|===
-| Type | Agg-based | Markdown | Lens | TSVB
+| Type | Lens | TSVB | Agg-based
| Metrics with filters
+| ✓
|
|
-^| X
-|
-
-| Average
-^| X
-^| X
-^| X
-^| X
-| Sum
-^| X
-^| X
-^| X
-^| X
+| Average, Sum, Max, Min
+| ✓
+| ✓
+| ✓
| Unique count (Cardinality)
-^| X
-^| X
-^| X
-^| X
-
-| Max
-^| X
-^| X
-^| X
-^| X
-
-| Min
-^| X
-^| X
-^| X
-^| X
-
-| Percentiles
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
+
+| Percentiles and Median
+| ✓
+| ✓
+| ✓
| Percentiles Rank
-^| X
-^| X
-|
-^| X
+|
+| ✓
+| ✓
+
+| Standard deviation
+|
+| ✓
+| ✓
+
+| Sum of squares
+|
+| ✓
+|
| Top hit (Last value)
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Value count
|
|
+| ✓
+
+| Variance
+|
+| ✓
|
-^| X
|===
@@ -261,61 +318,94 @@ For information about {es} pipeline aggregations, refer to {ref}/search-aggregat
[options="header"]
|===
-| Type | Agg-based | Markdown | Lens | TSVB
+| Type | Lens | TSVB | Agg-based
| Avg bucket
-^| X
-^| X
-|
-^| X
+| <>
+| ✓
+| ✓
| Derivative
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Max bucket
-^| X
-^| X
-|
-^| X
+| <>
+| ✓
+| ✓
| Min bucket
-^| X
-^| X
-|
-^| X
+| <>
+| ✓
+| ✓
| Sum bucket
-^| X
-^| X
-|
-^| X
+| <>
+| ✓
+| ✓
| Moving average
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Cumulative sum
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Bucket script
|
|
+| ✓
+
+| Bucket selector
+|
|
-^| X
+|
| Serial differencing
-^| X
-^| X
|
-^| X
+| ✓
+| ✓
+
+|===
+
+[float]
+[[custom-functions]]
+=== Additional functions
+
+[options="header"]
+|===
+
+| Type | Lens | TSVB | Agg-based
+
+| Counter rate
+| ✓
+| ✓
+|
+
+| <>
+| Use <>
+| ✓
+|
+
+| <>
+|
+| ✓
+|
+
+| <>
+|
+| ✓
+|
+
+| Static value
+|
+| ✓
+|
+
|===
@@ -329,41 +419,49 @@ build their advanced visualization.
[options="header"]
|===
-| Type | Agg-based | Lens | TSVB | Timelion | Vega
+| Type | Lens | TSVB | Agg-based | Vega | Timelion
-| Math on aggregated data
+| Math
+| ✓
+| ✓
|
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
| Visualize two indices
+| ✓
+| ✓
|
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
| Math across indices
|
|
|
-^| X
-^| X
+| ✓
+| ✓
| Time shifts
+| ✓
+| ✓
|
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
| Fully custom {es} queries
|
|
|
+| ✓
|
-^| X
+
+| Normalize by time
+| ✓
+| ✓
+|
+|
+|
+
|===
diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc
index 0eb4b43466ff9..84c33db31d575 100644
--- a/docs/user/dashboard/drilldowns.asciidoc
+++ b/docs/user/dashboard/drilldowns.asciidoc
@@ -112,7 +112,7 @@ The following panel types support drilldowns.
^| X
^| X
-| TSVB
+| TSVB (only for time series visualizations)
^| X
^| X
diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc
index 4ecfcc9250122..2071f17ecff3d 100644
--- a/docs/user/dashboard/lens.asciidoc
+++ b/docs/user/dashboard/lens.asciidoc
@@ -139,6 +139,42 @@ image::images/lens_drag_drop_3.gif[Using drag and drop to reorder]
. Press Space bar to confirm, or to cancel, press Esc.
+[float]
+[[lens-formulas]]
+==== Use formulas to perform math
+
+Formulas let you perform math on aggregated data in Lens by typing
+math and quick functions. To access formulas,
+click the *Formula* tab in the dimension editor. Access the complete
+reference for formulas from the help menu.
+
+The most common formulas are dividing two values to produce a percent.
+To display accurately, set *Value format* to *Percent*.
+
+Filter ratio::
+
+Use `kql=''` to filter one set of documents and compare it to other documents within the same grouping.
+For example, to see how the error rate changes over time:
++
+```
+count(kql='response.status_code > 400') / count()
+```
+
+Week over week:: Use `shift='1w'` to get the value of each grouping from
+the previous week. Time shift should not be used with the *Top values* function.
++
+```
+percentile(system.network.in.bytes, percentile=99) /
+percentile(system.network.in.bytes, percentile=99, shift='1w')
+```
+
+Percent of total:: Formulas can calculate `overall_sum` for all the groupings,
+which lets you convert each grouping into a percent of total:
++
+```
+sum(products.base_price) / overall_sum(sum(products.base_price))
+```
+
[float]
[[lens-faq]]
==== Frequently asked questions
diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc
index cc384ec041a9d..6829e129cd3b6 100644
--- a/docs/user/dashboard/vega-reference.asciidoc
+++ b/docs/user/dashboard/vega-reference.asciidoc
@@ -50,6 +50,11 @@ To learn more, read about
https://vega.github.io/vega/docs/specification/#autosize[autosize]
in the Vega documentation.
+WARNING: Autosize in Vega-Lite has https://vega.github.io/vega-lite/docs/size.html#limitations[several limitations]
+that can result in a warning like `Autosize "fit" only works for single views and layered views.`
+The recommended fix for this warning is to convert your spec to Vega using the <>
+`VEGA_DEBUG.vega_spec` output.
+
[float]
[[vega-theme]]
====== Default theme to match {kib}
diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc
index b9fc0c9c4ac46..5808e56d6d289 100644
--- a/docs/user/security/audit-logging.asciidoc
+++ b/docs/user/security/audit-logging.asciidoc
@@ -93,9 +93,9 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is creating a connector.
| `failure` | User is not authorized to create a connector.
-.2+| `alert_create`
-| `unknown` | User is creating an alert.
-| `failure` | User is not authorized to create an alert.
+.2+| `rule_create`
+| `unknown` | User is creating a rule.
+| `failure` | User is not authorized to create a rule.
.2+| `space_create`
| `unknown` | User is creating a space.
@@ -128,38 +128,38 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is updating a connector.
| `failure` | User is not authorized to update a connector.
-.2+| `alert_update`
-| `unknown` | User is updating an alert.
-| `failure` | User is not authorized to update an alert.
+.2+| `rule_update`
+| `unknown` | User is updating a rule.
+| `failure` | User is not authorized to update a rule.
-.2+| `alert_update_api_key`
-| `unknown` | User is updating the API key of an alert.
-| `failure` | User is not authorized to update the API key of an alert.
+.2+| `rule_update_api_key`
+| `unknown` | User is updating the API key of a rule.
+| `failure` | User is not authorized to update the API key of a rule.
-.2+| `alert_enable`
-| `unknown` | User is enabling an alert.
-| `failure` | User is not authorized to enable an alert.
+.2+| `rule_enable`
+| `unknown` | User is enabling a rule.
+| `failure` | User is not authorized to enable a rule.
-.2+| `alert_disable`
-| `unknown` | User is disabling an alert.
-| `failure` | User is not authorized to disable an alert.
+.2+| `rule_disable`
+| `unknown` | User is disabling a rule.
+| `failure` | User is not authorized to disable a rule.
-.2+| `alert_mute`
+.2+| `rule_mute`
+| `unknown` | User is muting a rule.
+| `failure` | User is not authorized to mute a rule.
+
+.2+| `rule_unmute`
+| `unknown` | User is unmuting a rule.
+| `failure` | User is not authorized to unmute a rule.
+
+.2+| `rule_alert_mute`
| `unknown` | User is muting an alert.
| `failure` | User is not authorized to mute an alert.
-.2+| `alert_unmute`
+.2+| `rule_alert_unmute`
| `unknown` | User is unmuting an alert.
| `failure` | User is not authorized to unmute an alert.
-.2+| `alert_instance_mute`
-| `unknown` | User is muting an alert instance.
-| `failure` | User is not authorized to mute an alert instance.
-
-.2+| `alert_instance_unmute`
-| `unknown` | User is unmuting an alert instance.
-| `failure` | User is not authorized to unmute an alert instance.
-
.2+| `space_update`
| `unknown` | User is updating a space.
| `failure` | User is not authorized to update a space.
@@ -183,9 +183,9 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is deleting a connector.
| `failure` | User is not authorized to delete a connector.
-.2+| `alert_delete`
-| `unknown` | User is deleting an alert.
-| `failure` | User is not authorized to delete an alert.
+.2+| `rule_delete`
+| `unknown` | User is deleting a rule.
+| `failure` | User is not authorized to delete a rule.
.2+| `space_delete`
| `unknown` | User is deleting a space.
@@ -218,13 +218,13 @@ Refer to the corresponding {es} logs for potential write errors.
| `success` | User has accessed a connector as part of a search operation.
| `failure` | User is not authorized to search for connectors.
-.2+| `alert_get`
-| `success` | User has accessed an alert.
-| `failure` | User is not authorized to access an alert.
+.2+| `rule_get`
+| `success` | User has accessed a rule.
+| `failure` | User is not authorized to access a rule.
-.2+| `alert_find`
-| `success` | User has accessed an alert as part of a search operation.
-| `failure` | User is not authorized to search for alerts.
+.2+| `rule_find`
+| `success` | User has accessed a rule as part of a search operation.
+| `failure` | User is not authorized to search for rules.
.2+| `space_get`
| `success` | User has accessed a space.
diff --git a/jest.config.integration.js b/jest.config.integration.js
index 50767932a52d7..b6ecb4569b643 100644
--- a/jest.config.integration.js
+++ b/jest.config.integration.js
@@ -13,7 +13,6 @@ module.exports = {
rootDir: '.',
roots: ['/src', '/packages'],
testMatch: ['**/integration_tests**/*.test.{js,mjs,ts,tsx}'],
- testRunner: 'jasmine2',
testPathIgnorePatterns: preset.testPathIgnorePatterns.filter(
(pattern) => !pattern.includes('integration_tests')
),
diff --git a/package.json b/package.json
index 310350baf7b2d..f99eb86a43cec 100644
--- a/package.json
+++ b/package.json
@@ -97,13 +97,13 @@
"yarn": "^1.21.1"
},
"dependencies": {
- "@elastic/apm-rum": "^5.6.1",
- "@elastic/apm-rum-react": "^1.2.5",
+ "@elastic/apm-rum": "^5.8.0",
+ "@elastic/apm-rum-react": "^1.2.11",
"@elastic/charts": "30.1.0",
"@elastic/datemath": "link:bazel-bin/packages/elastic-datemath",
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13",
- "@elastic/ems-client": "7.13.0",
- "@elastic/eui": "33.0.0",
+ "@elastic/ems-client": "7.14.0",
+ "@elastic/eui": "34.3.0",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "^9.0.1-kibana3",
"@elastic/maki": "6.3.0",
@@ -133,7 +133,7 @@
"@kbn/crypto": "link:bazel-bin/packages/kbn-crypto",
"@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl",
"@kbn/i18n": "link:bazel-bin/packages/kbn-i18n",
- "@kbn/interpreter": "link:packages/kbn-interpreter",
+ "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter",
"@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils",
"@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging",
"@kbn/logging": "link:bazel-bin/packages/kbn-logging",
@@ -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",
@@ -224,7 +227,7 @@
"deep-freeze-strict": "^1.1.1",
"deepmerge": "^4.2.2",
"del": "^5.1.0",
- "elastic-apm-node": "^3.14.0",
+ "elastic-apm-node": "^3.16.0",
"elasticsearch": "^16.7.0",
"execa": "^4.0.2",
"exit-hook": "^2.2.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,11 +465,11 @@
"@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",
- "@kbn/storybook": "link:packages/kbn-storybook",
+ "@kbn/storybook": "link:bazel-bin/packages/kbn-storybook",
"@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools",
"@kbn/test": "link:packages/kbn-test",
"@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector",
@@ -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",
@@ -841,4 +843,4 @@
"yargs": "^15.4.1",
"zlib": "^1.0.5"
}
-}
\ No newline at end of file
+}
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index 6208910729625..d9e2f0e1f9985 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -3,7 +3,7 @@
filegroup(
name = "build",
srcs = [
- "//packages/elastic-datemath:build",
+ "//packages/elastic-datemath:build",
"//packages/elastic-eslint-config-kibana:build",
"//packages/elastic-safer-lodash-set:build",
"//packages/kbn-ace:build",
@@ -23,11 +23,13 @@ filegroup(
"//packages/kbn-eslint-plugin-eslint:build",
"//packages/kbn-expect:build",
"//packages/kbn-i18n:build",
+ "//packages/kbn-interpreter:build",
"//packages/kbn-io-ts-utils:build",
"//packages/kbn-legacy-logging:build",
"//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",
@@ -40,12 +42,15 @@ 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",
"//packages/kbn-std:build",
+ "//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 dd491de55c075..cf6fcfd88a26d 100644
--- a/packages/kbn-cli-dev-mode/package.json
+++ b/packages/kbn-cli-dev-mode/package.json
@@ -12,8 +12,5 @@
},
"kibana": {
"devOnly": true
- },
- "dependencies": {
- "@kbn/optimizer": "link:../kbn-optimizer"
}
}
\ No newline at end of file
diff --git a/packages/kbn-interpreter/.babelrc b/packages/kbn-interpreter/.babelrc
deleted file mode 100644
index 309b3d5b3233d..0000000000000
--- a/packages/kbn-interpreter/.babelrc
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "presets": ["@kbn/babel-preset/webpack_preset"],
- "plugins": [
- "@babel/plugin-transform-modules-commonjs",
- ["@babel/plugin-transform-runtime", {
- "regenerator": true
- }]
- ]
-}
diff --git a/packages/kbn-interpreter/.npmignore b/packages/kbn-interpreter/.npmignore
deleted file mode 100644
index b9bc539e63ce4..0000000000000
--- a/packages/kbn-interpreter/.npmignore
+++ /dev/null
@@ -1,3 +0,0 @@
-src
-tasks
-.babelrc
diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel
new file mode 100644
index 0000000000000..4492faabfdf81
--- /dev/null
+++ b/packages/kbn-interpreter/BUILD.bazel
@@ -0,0 +1,99 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+load("@npm//pegjs:index.bzl", "pegjs")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-interpreter"
+PKG_REQUIRE_NAME = "@kbn/interpreter"
+
+SOURCE_FILES = glob(
+ [
+ "src/**/*",
+ ]
+)
+
+TYPE_FILES = []
+
+SRCS = SOURCE_FILES + TYPE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "common/package.json",
+ "package.json",
+]
+
+SRC_DEPS = [
+ "@npm//lodash",
+]
+
+TYPES_DEPS = [
+ "@npm//@types/jest",
+ "@npm//@types/lodash",
+ "@npm//@types/node",
+]
+
+DEPS = SRC_DEPS + TYPES_DEPS
+
+pegjs(
+ name = "grammar",
+ data = [
+ ":grammar/grammar.pegjs"
+ ],
+ output_dir = True,
+ args = [
+ "--allowed-start-rules",
+ "expression,argument",
+ "-o",
+ "$(@D)/index.js",
+ "./%s/grammar/grammar.pegjs" % package_name()
+ ],
+)
+
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//:tsconfig.base.json",
+ ],
+)
+
+ts_project(
+ name = "tsc",
+ args = ['--pretty'],
+ srcs = SRCS,
+ deps = DEPS,
+ allow_js = True,
+ 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 + [":grammar"],
+ 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-interpreter/common/package.json b/packages/kbn-interpreter/common/package.json
index 62061138234d9..2f5277a8e8652 100644
--- a/packages/kbn-interpreter/common/package.json
+++ b/packages/kbn-interpreter/common/package.json
@@ -1,6 +1,5 @@
{
"private": true,
"main": "../target/common/index.js",
- "types": "../target/common/index.d.ts",
- "jsnext:main": "../src/common/index.js"
+ "types": "../target/common/index.d.ts"
}
\ No newline at end of file
diff --git a/packages/kbn-interpreter/src/common/lib/grammar.peg b/packages/kbn-interpreter/grammar/grammar.pegjs
similarity index 100%
rename from packages/kbn-interpreter/src/common/lib/grammar.peg
rename to packages/kbn-interpreter/grammar/grammar.pegjs
diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json
index fc0936f4b5f53..efdb30e105186 100644
--- a/packages/kbn-interpreter/package.json
+++ b/packages/kbn-interpreter/package.json
@@ -2,11 +2,5 @@
"name": "@kbn/interpreter",
"private": "true",
"version": "1.0.0",
- "license": "SSPL-1.0 OR Elastic License 2.0",
- "scripts": {
- "interpreter:peg": "../../node_modules/.bin/pegjs src/common/lib/grammar.peg",
- "build": "node scripts/build",
- "kbn:bootstrap": "node scripts/build --dev",
- "kbn:watch": "node scripts/build --dev --watch"
- }
+ "license": "SSPL-1.0 OR Elastic License 2.0"
}
\ No newline at end of file
diff --git a/packages/kbn-interpreter/src/common/index.js b/packages/kbn-interpreter/src/common/index.ts
similarity index 76%
rename from packages/kbn-interpreter/src/common/index.js
rename to packages/kbn-interpreter/src/common/index.ts
index b83d8180980cd..524c854b40429 100644
--- a/packages/kbn-interpreter/src/common/index.js
+++ b/packages/kbn-interpreter/src/common/index.ts
@@ -6,11 +6,19 @@
* Side Public License, v 1.
*/
-export { fromExpression, toExpression, safeElementFromExpression } from './lib/ast';
+export {
+ fromExpression,
+ toExpression,
+ safeElementFromExpression,
+ Ast,
+ ExpressionFunctionAST,
+} from './lib/ast';
export { Fn } from './lib/fn';
export { getType } from './lib/get_type';
export { castProvider } from './lib/cast';
-export { parse } from './lib/grammar';
+// @ts-expect-error
+// @internal
+export { parse } from '../../grammar';
export { getByAlias } from './lib/get_by_alias';
export { Registry } from './lib/registry';
export { addRegistries, register, registryFactory } from './registries';
diff --git a/packages/kbn-interpreter/src/common/lib/ast.d.ts b/packages/kbn-interpreter/src/common/lib/ast.d.ts
deleted file mode 100644
index 0e95cb9901df0..0000000000000
--- a/packages/kbn-interpreter/src/common/lib/ast.d.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 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 type ExpressionArgAST = string | boolean | number | Ast;
-
-export interface ExpressionFunctionAST {
- type: 'function';
- function: string;
- arguments: {
- [key: string]: ExpressionArgAST[];
- };
-}
-
-export interface Ast {
- type: 'expression';
- chain: ExpressionFunctionAST[];
-}
-
-export declare function fromExpression(expression: string): Ast;
-export declare function toExpression(astObj: Ast, type?: string): string;
diff --git a/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js b/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js
index c67a266e1276a..a098a3fdce0f6 100644
--- a/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js
+++ b/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { fromExpression } from './ast';
+import { fromExpression } from '@kbn/interpreter/target/common/lib/ast';
import { getType } from './get_type';
describe('ast fromExpression', () => {
diff --git a/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js b/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js
index c60412f05c15a..b500ca06836a4 100644
--- a/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js
+++ b/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { toExpression } from './ast';
+import { toExpression } from '@kbn/interpreter/common';
describe('ast toExpression', () => {
describe('single expression', () => {
diff --git a/packages/kbn-interpreter/src/common/lib/ast.js b/packages/kbn-interpreter/src/common/lib/ast.ts
similarity index 75%
rename from packages/kbn-interpreter/src/common/lib/ast.js
rename to packages/kbn-interpreter/src/common/lib/ast.ts
index fb471e34ccc69..791c94809f35c 100644
--- a/packages/kbn-interpreter/src/common/lib/ast.js
+++ b/packages/kbn-interpreter/src/common/lib/ast.ts
@@ -7,12 +7,35 @@
*/
import { getType } from './get_type';
-import { parse } from './grammar';
+// @ts-expect-error
+import { parse } from '../../../grammar';
-function getArgumentString(arg, argKey, level = 0) {
+export type ExpressionArgAST = string | boolean | number | Ast;
+
+export interface ExpressionFunctionAST {
+ type: 'function';
+ function: string;
+ arguments: {
+ [key: string]: ExpressionArgAST[];
+ };
+}
+
+export interface Ast {
+ /** @internal */
+ function: any;
+ /** @internal */
+ arguments: any;
+ type: 'expression';
+ chain: ExpressionFunctionAST[];
+ /** @internal */
+ replace(regExp: RegExp, s: string): string;
+}
+
+function getArgumentString(arg: Ast, argKey: string | undefined, level = 0) {
const type = getType(arg);
- function maybeArgKey(argKey, argString) {
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ function maybeArgKey(argKey: string | null | undefined, argString: string) {
return argKey == null || argKey === '_' ? argString : `${argKey}=${argString}`;
}
@@ -36,7 +59,7 @@ function getArgumentString(arg, argKey, level = 0) {
throw new Error(`Invalid argument type in AST: ${type}`);
}
-function getExpressionArgs(block, level = 0) {
+function getExpressionArgs(block: Ast, level = 0) {
const args = block.arguments;
const hasValidArgs = typeof args === 'object' && args != null && !Array.isArray(args);
@@ -45,7 +68,7 @@ function getExpressionArgs(block, level = 0) {
const argKeys = Object.keys(args);
const MAX_LINE_LENGTH = 80; // length before wrapping arguments
return argKeys.map((argKey) =>
- args[argKey].reduce((acc, arg) => {
+ args[argKey].reduce((acc: any, arg: any) => {
const argString = getArgumentString(arg, argKey, level);
const lineLength = acc.split('\n').pop().length;
@@ -63,12 +86,12 @@ function getExpressionArgs(block, level = 0) {
);
}
-function fnWithArgs(fnName, args) {
+function fnWithArgs(fnName: any, args: any[]) {
if (!args || args.length === 0) return fnName;
return `${fnName} ${args.join(' ')}`;
}
-function getExpression(chain, level = 0) {
+function getExpression(chain: any[], level = 0) {
if (!chain) throw new Error('Expressions must contain a chain');
// break new functions onto new lines if we're not in a nested/sub-expression
@@ -90,7 +113,7 @@ function getExpression(chain, level = 0) {
.join(separator);
}
-export function fromExpression(expression, type = 'expression') {
+export function fromExpression(expression: string, type = 'expression'): Ast {
try {
return parse(String(expression), { startRule: type });
} catch (e) {
@@ -99,7 +122,7 @@ export function fromExpression(expression, type = 'expression') {
}
// TODO: OMG This is so bad, we need to talk about the right way to handle bad expressions since some are element based and others not
-export function safeElementFromExpression(expression) {
+export function safeElementFromExpression(expression: string) {
try {
return fromExpression(expression);
} catch (e) {
@@ -116,8 +139,11 @@ Thanks for understanding,
}
// TODO: Respect the user's existing formatting
-export function toExpression(astObj, type = 'expression') {
- if (type === 'argument') return getArgumentString(astObj);
+export function toExpression(astObj: Ast, type = 'expression'): string {
+ if (type === 'argument') {
+ // @ts-ignore
+ return getArgumentString(astObj);
+ }
const validType = ['expression', 'function'].includes(getType(astObj));
if (!validType) throw new Error('Expression must be an expression or argument function');
diff --git a/packages/kbn-interpreter/src/common/lib/get_type.js b/packages/kbn-interpreter/src/common/lib/get_type.ts
similarity index 92%
rename from packages/kbn-interpreter/src/common/lib/get_type.js
rename to packages/kbn-interpreter/src/common/lib/get_type.ts
index 7ae6dab029176..b6dff67bf5dc9 100644
--- a/packages/kbn-interpreter/src/common/lib/get_type.js
+++ b/packages/kbn-interpreter/src/common/lib/get_type.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-export function getType(node) {
+export function getType(node: any): string {
if (node == null) return 'null';
if (typeof node === 'object') {
if (!node.type) throw new Error('Objects must have a type property');
diff --git a/packages/kbn-interpreter/src/common/lib/grammar.js b/packages/kbn-interpreter/src/common/lib/grammar.js
deleted file mode 100644
index 3f473b1beea63..0000000000000
--- a/packages/kbn-interpreter/src/common/lib/grammar.js
+++ /dev/null
@@ -1,1053 +0,0 @@
-/*
- * Generated by PEG.js 0.10.0.
- *
- * http://pegjs.org/
- */
-
-"use strict";
-
-function peg$subclass(child, parent) {
- function ctor() { this.constructor = child; }
- ctor.prototype = parent.prototype;
- child.prototype = new ctor();
-}
-
-function peg$SyntaxError(message, expected, found, location) {
- this.message = message;
- this.expected = expected;
- this.found = found;
- this.location = location;
- this.name = "SyntaxError";
-
- if (typeof Error.captureStackTrace === "function") {
- Error.captureStackTrace(this, peg$SyntaxError);
- }
-}
-
-peg$subclass(peg$SyntaxError, Error);
-
-peg$SyntaxError.buildMessage = function(expected, found) {
- var DESCRIBE_EXPECTATION_FNS = {
- literal: function(expectation) {
- return "\"" + literalEscape(expectation.text) + "\"";
- },
-
- "class": function(expectation) {
- var escapedParts = "",
- i;
-
- for (i = 0; i < expectation.parts.length; i++) {
- escapedParts += expectation.parts[i] instanceof Array
- ? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1])
- : classEscape(expectation.parts[i]);
- }
-
- return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]";
- },
-
- any: function(expectation) {
- return "any character";
- },
-
- end: function(expectation) {
- return "end of input";
- },
-
- other: function(expectation) {
- return expectation.description;
- }
- };
-
- function hex(ch) {
- return ch.charCodeAt(0).toString(16).toUpperCase();
- }
-
- function literalEscape(s) {
- return s
- .replace(/\\/g, '\\\\')
- .replace(/"/g, '\\"')
- .replace(/\0/g, '\\0')
- .replace(/\t/g, '\\t')
- .replace(/\n/g, '\\n')
- .replace(/\r/g, '\\r')
- .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
- .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); });
- }
-
- function classEscape(s) {
- return s
- .replace(/\\/g, '\\\\')
- .replace(/\]/g, '\\]')
- .replace(/\^/g, '\\^')
- .replace(/-/g, '\\-')
- .replace(/\0/g, '\\0')
- .replace(/\t/g, '\\t')
- .replace(/\n/g, '\\n')
- .replace(/\r/g, '\\r')
- .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
- .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); });
- }
-
- function describeExpectation(expectation) {
- return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
- }
-
- function describeExpected(expected) {
- var descriptions = new Array(expected.length),
- i, j;
-
- for (i = 0; i < expected.length; i++) {
- descriptions[i] = describeExpectation(expected[i]);
- }
-
- descriptions.sort();
-
- if (descriptions.length > 0) {
- for (i = 1, j = 1; i < descriptions.length; i++) {
- if (descriptions[i - 1] !== descriptions[i]) {
- descriptions[j] = descriptions[i];
- j++;
- }
- }
- descriptions.length = j;
- }
-
- switch (descriptions.length) {
- case 1:
- return descriptions[0];
-
- case 2:
- return descriptions[0] + " or " + descriptions[1];
-
- default:
- return descriptions.slice(0, -1).join(", ")
- + ", or "
- + descriptions[descriptions.length - 1];
- }
- }
-
- function describeFound(found) {
- return found ? "\"" + literalEscape(found) + "\"" : "end of input";
- }
-
- return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
-};
-
-function peg$parse(input, options) {
- options = options !== void 0 ? options : {};
-
- var peg$FAILED = {},
-
- peg$startRuleFunctions = { expression: peg$parseexpression, argument: peg$parseargument },
- peg$startRuleFunction = peg$parseexpression,
-
- peg$c0 = "|",
- peg$c1 = peg$literalExpectation("|", false),
- peg$c2 = function(first, fn) { return fn; },
- peg$c3 = function(first, rest) {
- return addMeta({
- type: 'expression',
- chain: first ? [first].concat(rest) : []
- }, text(), location());
- },
- peg$c4 = peg$otherExpectation("function"),
- peg$c5 = function(name, arg_list) {
- return addMeta({
- type: 'function',
- function: name,
- arguments: arg_list
- }, text(), location());
- },
- peg$c6 = "=",
- peg$c7 = peg$literalExpectation("=", false),
- peg$c8 = function(name, value) {
- return { name, value };
- },
- peg$c9 = function(value) {
- return { name: '_', value };
- },
- peg$c10 = "$",
- peg$c11 = peg$literalExpectation("$", false),
- peg$c12 = "{",
- peg$c13 = peg$literalExpectation("{", false),
- peg$c14 = "}",
- peg$c15 = peg$literalExpectation("}", false),
- peg$c16 = function(expression) { return expression; },
- peg$c17 = function(value) {
- return addMeta(value, text(), location());
- },
- peg$c18 = function(arg) { return arg; },
- peg$c19 = function(args) {
- return args.reduce((accumulator, { name, value }) => ({
- ...accumulator,
- [name]: (accumulator[name] || []).concat(value)
- }), {});
- },
- peg$c20 = /^[a-zA-Z0-9_\-]/,
- peg$c21 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"], "_", "-"], false, false),
- peg$c22 = function(name) {
- return name.join('');
- },
- peg$c23 = peg$otherExpectation("literal"),
- peg$c24 = "\"",
- peg$c25 = peg$literalExpectation("\"", false),
- peg$c26 = function(chars) { return chars.join(''); },
- peg$c27 = "'",
- peg$c28 = peg$literalExpectation("'", false),
- peg$c29 = function(string) { // this also matches nulls, booleans, and numbers
- var result = string.join('');
- // Sort of hacky, but PEG doesn't have backtracking so
- // a null/boolean/number rule is hard to read, and performs worse
- if (result === 'null') return null;
- if (result === 'true') return true;
- if (result === 'false') return false;
- if (isNaN(Number(result))) return result; // 5bears
- return Number(result);
- },
- peg$c30 = /^[ \t\r\n]/,
- peg$c31 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false),
- peg$c32 = "\\",
- peg$c33 = peg$literalExpectation("\\", false),
- peg$c34 = /^["'(){}<>[\]$`|= \t\n\r]/,
- peg$c35 = peg$classExpectation(["\"", "'", "(", ")", "{", "}", "<", ">", "[", "]", "$", "`", "|", "=", " ", "\t", "\n", "\r"], false, false),
- peg$c36 = function(sequence) { return sequence; },
- peg$c37 = /^[^"'(){}<>[\]$`|= \t\n\r]/,
- peg$c38 = peg$classExpectation(["\"", "'", "(", ")", "{", "}", "<", ">", "[", "]", "$", "`", "|", "=", " ", "\t", "\n", "\r"], true, false),
- peg$c39 = /^[^"]/,
- peg$c40 = peg$classExpectation(["\""], true, false),
- peg$c41 = /^[^']/,
- peg$c42 = peg$classExpectation(["'"], true, false),
-
- peg$currPos = 0,
- peg$savedPos = 0,
- peg$posDetailsCache = [{ line: 1, column: 1 }],
- peg$maxFailPos = 0,
- peg$maxFailExpected = [],
- peg$silentFails = 0,
-
- peg$result;
-
- if ("startRule" in options) {
- if (!(options.startRule in peg$startRuleFunctions)) {
- throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
- }
-
- peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
- }
-
- function text() {
- return input.substring(peg$savedPos, peg$currPos);
- }
-
- function location() {
- return peg$computeLocation(peg$savedPos, peg$currPos);
- }
-
- function expected(description, location) {
- location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos)
-
- throw peg$buildStructuredError(
- [peg$otherExpectation(description)],
- input.substring(peg$savedPos, peg$currPos),
- location
- );
- }
-
- function error(message, location) {
- location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos)
-
- throw peg$buildSimpleError(message, location);
- }
-
- function peg$literalExpectation(text, ignoreCase) {
- return { type: "literal", text: text, ignoreCase: ignoreCase };
- }
-
- function peg$classExpectation(parts, inverted, ignoreCase) {
- return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase };
- }
-
- function peg$anyExpectation() {
- return { type: "any" };
- }
-
- function peg$endExpectation() {
- return { type: "end" };
- }
-
- function peg$otherExpectation(description) {
- return { type: "other", description: description };
- }
-
- function peg$computePosDetails(pos) {
- var details = peg$posDetailsCache[pos], p;
-
- if (details) {
- return details;
- } else {
- p = pos - 1;
- while (!peg$posDetailsCache[p]) {
- p--;
- }
-
- details = peg$posDetailsCache[p];
- details = {
- line: details.line,
- column: details.column
- };
-
- while (p < pos) {
- if (input.charCodeAt(p) === 10) {
- details.line++;
- details.column = 1;
- } else {
- details.column++;
- }
-
- p++;
- }
-
- peg$posDetailsCache[pos] = details;
- return details;
- }
- }
-
- function peg$computeLocation(startPos, endPos) {
- var startPosDetails = peg$computePosDetails(startPos),
- endPosDetails = peg$computePosDetails(endPos);
-
- return {
- start: {
- offset: startPos,
- line: startPosDetails.line,
- column: startPosDetails.column
- },
- end: {
- offset: endPos,
- line: endPosDetails.line,
- column: endPosDetails.column
- }
- };
- }
-
- function peg$fail(expected) {
- if (peg$currPos < peg$maxFailPos) { return; }
-
- if (peg$currPos > peg$maxFailPos) {
- peg$maxFailPos = peg$currPos;
- peg$maxFailExpected = [];
- }
-
- peg$maxFailExpected.push(expected);
- }
-
- function peg$buildSimpleError(message, location) {
- return new peg$SyntaxError(message, null, null, location);
- }
-
- function peg$buildStructuredError(expected, found, location) {
- return new peg$SyntaxError(
- peg$SyntaxError.buildMessage(expected, found),
- expected,
- found,
- location
- );
- }
-
- function peg$parseexpression() {
- var s0, s1, s2, s3, s4, s5, s6, s7;
-
- s0 = peg$currPos;
- s1 = peg$parsespace();
- if (s1 === peg$FAILED) {
- s1 = null;
- }
- if (s1 !== peg$FAILED) {
- s2 = peg$parsefunction();
- if (s2 === peg$FAILED) {
- s2 = null;
- }
- if (s2 !== peg$FAILED) {
- s3 = [];
- s4 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 124) {
- s5 = peg$c0;
- peg$currPos++;
- } else {
- s5 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c1); }
- }
- if (s5 !== peg$FAILED) {
- s6 = peg$parsespace();
- if (s6 === peg$FAILED) {
- s6 = null;
- }
- if (s6 !== peg$FAILED) {
- s7 = peg$parsefunction();
- if (s7 !== peg$FAILED) {
- peg$savedPos = s4;
- s5 = peg$c2(s2, s7);
- s4 = s5;
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- while (s4 !== peg$FAILED) {
- s3.push(s4);
- s4 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 124) {
- s5 = peg$c0;
- peg$currPos++;
- } else {
- s5 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c1); }
- }
- if (s5 !== peg$FAILED) {
- s6 = peg$parsespace();
- if (s6 === peg$FAILED) {
- s6 = null;
- }
- if (s6 !== peg$FAILED) {
- s7 = peg$parsefunction();
- if (s7 !== peg$FAILED) {
- peg$savedPos = s4;
- s5 = peg$c2(s2, s7);
- s4 = s5;
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- }
- if (s3 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c3(s2, s3);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
-
- return s0;
- }
-
- function peg$parsefunction() {
- var s0, s1, s2;
-
- peg$silentFails++;
- s0 = peg$currPos;
- s1 = peg$parseidentifier();
- if (s1 !== peg$FAILED) {
- s2 = peg$parsearg_list();
- if (s2 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c5(s1, s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- peg$silentFails--;
- if (s0 === peg$FAILED) {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c4); }
- }
-
- return s0;
- }
-
- function peg$parseargument_assignment() {
- var s0, s1, s2, s3, s4, s5;
-
- s0 = peg$currPos;
- s1 = peg$parseidentifier();
- if (s1 !== peg$FAILED) {
- s2 = peg$parsespace();
- if (s2 === peg$FAILED) {
- s2 = null;
- }
- if (s2 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 61) {
- s3 = peg$c6;
- peg$currPos++;
- } else {
- s3 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c7); }
- }
- if (s3 !== peg$FAILED) {
- s4 = peg$parsespace();
- if (s4 === peg$FAILED) {
- s4 = null;
- }
- if (s4 !== peg$FAILED) {
- s5 = peg$parseargument();
- if (s5 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c8(s1, s5);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- s0 = peg$currPos;
- s1 = peg$parseargument();
- if (s1 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c9(s1);
- }
- s0 = s1;
- }
-
- return s0;
- }
-
- function peg$parseargument() {
- var s0, s1, s2, s3, s4;
-
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 36) {
- s1 = peg$c10;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c11); }
- }
- if (s1 === peg$FAILED) {
- s1 = null;
- }
- if (s1 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 123) {
- s2 = peg$c12;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c13); }
- }
- if (s2 !== peg$FAILED) {
- s3 = peg$parseexpression();
- if (s3 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 125) {
- s4 = peg$c14;
- peg$currPos++;
- } else {
- s4 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c15); }
- }
- if (s4 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c16(s3);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- s0 = peg$currPos;
- s1 = peg$parseliteral();
- if (s1 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c17(s1);
- }
- s0 = s1;
- }
-
- return s0;
- }
-
- function peg$parsearg_list() {
- var s0, s1, s2, s3, s4;
-
- s0 = peg$currPos;
- s1 = [];
- s2 = peg$currPos;
- s3 = peg$parsespace();
- if (s3 !== peg$FAILED) {
- s4 = peg$parseargument_assignment();
- if (s4 !== peg$FAILED) {
- peg$savedPos = s2;
- s3 = peg$c18(s4);
- s2 = s3;
- } else {
- peg$currPos = s2;
- s2 = peg$FAILED;
- }
- } else {
- peg$currPos = s2;
- s2 = peg$FAILED;
- }
- while (s2 !== peg$FAILED) {
- s1.push(s2);
- s2 = peg$currPos;
- s3 = peg$parsespace();
- if (s3 !== peg$FAILED) {
- s4 = peg$parseargument_assignment();
- if (s4 !== peg$FAILED) {
- peg$savedPos = s2;
- s3 = peg$c18(s4);
- s2 = s3;
- } else {
- peg$currPos = s2;
- s2 = peg$FAILED;
- }
- } else {
- peg$currPos = s2;
- s2 = peg$FAILED;
- }
- }
- if (s1 !== peg$FAILED) {
- s2 = peg$parsespace();
- if (s2 === peg$FAILED) {
- s2 = null;
- }
- if (s2 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c19(s1);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
-
- return s0;
- }
-
- function peg$parseidentifier() {
- var s0, s1, s2;
-
- s0 = peg$currPos;
- s1 = [];
- if (peg$c20.test(input.charAt(peg$currPos))) {
- s2 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c21); }
- }
- if (s2 !== peg$FAILED) {
- while (s2 !== peg$FAILED) {
- s1.push(s2);
- if (peg$c20.test(input.charAt(peg$currPos))) {
- s2 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c21); }
- }
- }
- } else {
- s1 = peg$FAILED;
- }
- if (s1 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c22(s1);
- }
- s0 = s1;
-
- return s0;
- }
-
- function peg$parseliteral() {
- var s0, s1;
-
- peg$silentFails++;
- s0 = peg$parsephrase();
- if (s0 === peg$FAILED) {
- s0 = peg$parseunquoted_string_or_number();
- }
- peg$silentFails--;
- if (s0 === peg$FAILED) {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c23); }
- }
-
- return s0;
- }
-
- function peg$parsephrase() {
- var s0, s1, s2, s3;
-
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 34) {
- s1 = peg$c24;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c25); }
- }
- if (s1 !== peg$FAILED) {
- s2 = [];
- s3 = peg$parsedq_char();
- while (s3 !== peg$FAILED) {
- s2.push(s3);
- s3 = peg$parsedq_char();
- }
- if (s2 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 34) {
- s3 = peg$c24;
- peg$currPos++;
- } else {
- s3 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c25); }
- }
- if (s3 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c26(s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 39) {
- s1 = peg$c27;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c28); }
- }
- if (s1 !== peg$FAILED) {
- s2 = [];
- s3 = peg$parsesq_char();
- while (s3 !== peg$FAILED) {
- s2.push(s3);
- s3 = peg$parsesq_char();
- }
- if (s2 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 39) {
- s3 = peg$c27;
- peg$currPos++;
- } else {
- s3 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c28); }
- }
- if (s3 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c26(s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- }
-
- return s0;
- }
-
- function peg$parseunquoted_string_or_number() {
- var s0, s1, s2;
-
- s0 = peg$currPos;
- s1 = [];
- s2 = peg$parseunquoted();
- if (s2 !== peg$FAILED) {
- while (s2 !== peg$FAILED) {
- s1.push(s2);
- s2 = peg$parseunquoted();
- }
- } else {
- s1 = peg$FAILED;
- }
- if (s1 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c29(s1);
- }
- s0 = s1;
-
- return s0;
- }
-
- function peg$parsespace() {
- var s0, s1;
-
- s0 = [];
- if (peg$c30.test(input.charAt(peg$currPos))) {
- s1 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c31); }
- }
- if (s1 !== peg$FAILED) {
- while (s1 !== peg$FAILED) {
- s0.push(s1);
- if (peg$c30.test(input.charAt(peg$currPos))) {
- s1 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c31); }
- }
- }
- } else {
- s0 = peg$FAILED;
- }
-
- return s0;
- }
-
- function peg$parseunquoted() {
- var s0, s1, s2;
-
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 92) {
- s1 = peg$c32;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- if (s1 !== peg$FAILED) {
- if (peg$c34.test(input.charAt(peg$currPos))) {
- s2 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c35); }
- }
- if (s2 === peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 92) {
- s2 = peg$c32;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- }
- if (s2 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c36(s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- if (peg$c37.test(input.charAt(peg$currPos))) {
- s0 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s0 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c38); }
- }
- }
-
- return s0;
- }
-
- function peg$parsedq_char() {
- var s0, s1, s2;
-
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 92) {
- s1 = peg$c32;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- if (s1 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 34) {
- s2 = peg$c24;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c25); }
- }
- if (s2 === peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 92) {
- s2 = peg$c32;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- }
- if (s2 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c36(s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- if (peg$c39.test(input.charAt(peg$currPos))) {
- s0 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s0 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c40); }
- }
- }
-
- return s0;
- }
-
- function peg$parsesq_char() {
- var s0, s1, s2;
-
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 92) {
- s1 = peg$c32;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- if (s1 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 39) {
- s2 = peg$c27;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c28); }
- }
- if (s2 === peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 92) {
- s2 = peg$c32;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- }
- if (s2 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c36(s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- if (peg$c41.test(input.charAt(peg$currPos))) {
- s0 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s0 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c42); }
- }
- }
-
- return s0;
- }
-
-
- function addMeta(node, text, { start: { offset: start }, end: { offset: end } }) {
- if (!options.addMeta) return node;
- return { node, text, start, end };
- }
-
-
- peg$result = peg$startRuleFunction();
-
- if (peg$result !== peg$FAILED && peg$currPos === input.length) {
- return peg$result;
- } else {
- if (peg$result !== peg$FAILED && peg$currPos < input.length) {
- peg$fail(peg$endExpectation());
- }
-
- throw peg$buildStructuredError(
- peg$maxFailExpected,
- peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,
- peg$maxFailPos < input.length
- ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
- : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
- );
- }
-}
-
-module.exports = {
- SyntaxError: peg$SyntaxError,
- parse: peg$parse
-};
diff --git a/packages/kbn-interpreter/src/common/lib/registry.d.ts b/packages/kbn-interpreter/src/common/lib/registry.d.ts
deleted file mode 100644
index 766839ebf0e02..0000000000000
--- a/packages/kbn-interpreter/src/common/lib/registry.d.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 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 class Registry {
- constructor(prop?: string);
-
- public wrapper(obj: ItemSpec): Item;
-
- public register(fn: () => ItemSpec): void;
-
- public toJS(): { [key: string]: any };
-
- public toArray(): Item[];
-
- public get(name: string): Item;
-
- public getProp(): string;
-
- public reset(): void;
-}
diff --git a/packages/kbn-interpreter/src/common/lib/registry.js b/packages/kbn-interpreter/src/common/lib/registry.ts
similarity index 73%
rename from packages/kbn-interpreter/src/common/lib/registry.js
rename to packages/kbn-interpreter/src/common/lib/registry.ts
index 309f92ea24f6d..11f41ff736e96 100644
--- a/packages/kbn-interpreter/src/common/lib/registry.js
+++ b/packages/kbn-interpreter/src/common/lib/registry.ts
@@ -8,49 +8,59 @@
import { clone } from 'lodash';
-export class Registry {
+export class Registry {
+ private readonly _prop: string;
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ private _indexed: Object;
+
constructor(prop = 'name') {
if (typeof prop !== 'string') throw new Error('Registry property name must be a string');
this._prop = prop;
this._indexed = new Object();
}
- wrapper(obj) {
+ wrapper(obj: ItemSpec): Item {
+ // @ts-ignore
return obj;
}
- register(fn) {
+ register(fn: () => ItemSpec): void {
const obj = typeof fn === 'function' ? fn() : fn;
+ // @ts-ignore
if (typeof obj !== 'object' || !obj[this._prop]) {
throw new Error(`Registered functions must return an object with a ${this._prop} property`);
}
+ // @ts-ignore
this._indexed[obj[this._prop].toLowerCase()] = this.wrapper(obj);
}
- toJS() {
+ toJS(): { [key: string]: any } {
return Object.keys(this._indexed).reduce((acc, key) => {
+ // @ts-ignore
acc[key] = this.get(key);
return acc;
}, {});
}
- toArray() {
+ toArray(): Item[] {
return Object.keys(this._indexed).map((key) => this.get(key));
}
- get(name) {
+ get(name: string): Item {
+ // @ts-ignore
if (name === undefined) return null;
const lowerCaseName = name.toLowerCase();
+ // @ts-ignore
return this._indexed[lowerCaseName] ? clone(this._indexed[lowerCaseName]) : null;
}
- getProp() {
+ getProp(): string {
return this._prop;
}
- reset() {
+ reset(): void {
this._indexed = new Object();
}
}
diff --git a/packages/kbn-interpreter/tasks/build/__fixtures__/sample.js b/packages/kbn-interpreter/tasks/build/__fixtures__/sample.js
deleted file mode 100644
index f831545743f10..0000000000000
--- a/packages/kbn-interpreter/tasks/build/__fixtures__/sample.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* eslint-disable */
-import util from 'util';
-console.log(util.format('hello world'));
diff --git a/packages/kbn-interpreter/tasks/build/cli.js b/packages/kbn-interpreter/tasks/build/cli.js
deleted file mode 100644
index 82e4475b409c3..0000000000000
--- a/packages/kbn-interpreter/tasks/build/cli.js
+++ /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 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.
- */
-
-const { relative } = require('path');
-
-const getopts = require('getopts');
-const del = require('del');
-const supportsColor = require('supports-color');
-const { ToolingLog, withProcRunner, pickLevelFromFlags } = require('@kbn/dev-utils');
-
-const { ROOT_DIR, BUILD_DIR } = require('./paths');
-
-const unknownFlags = [];
-const flags = getopts(process.argv, {
- boolean: ['watch', 'dev', 'help', 'debug'],
- unknown(name) {
- unknownFlags.push(name);
- },
-});
-
-const log = new ToolingLog({
- level: pickLevelFromFlags(flags),
- writeTo: process.stdout,
-});
-
-if (unknownFlags.length) {
- log.error(`Unknown flag(s): ${unknownFlags.join(', ')}`);
- flags.help = true;
- process.exitCode = 1;
-}
-
-if (flags.help) {
- log.info(`
- Simple build tool for @kbn/interpreter package
-
- --dev Build for development, include source maps
- --watch Run in watch mode
- --debug Turn on debug logging
- `);
- process.exit();
-}
-
-withProcRunner(log, async (proc) => {
- log.info('Deleting old output');
- await del(BUILD_DIR);
-
- const cwd = ROOT_DIR;
- const env = { ...process.env };
- if (supportsColor.stdout) {
- env.FORCE_COLOR = 'true';
- }
-
- log.info(`Starting babel ${flags.watch ? ' in watch mode' : ''}`);
- await Promise.all([
- proc.run('babel ', {
- cmd: 'babel',
- args: [
- 'src',
- '--ignore',
- `*.test.js`,
- '--out-dir',
- relative(cwd, BUILD_DIR),
- '--copy-files',
- ...(flags.dev ? ['--source-maps', 'inline'] : []),
- ...(flags.watch ? ['--watch'] : ['--quiet']),
- ],
- wait: true,
- env,
- cwd,
- }),
- ]);
-
- log.success('Complete');
-}).catch((error) => {
- log.error(error);
- process.exit(1);
-});
diff --git a/packages/kbn-interpreter/tasks/build/paths.js b/packages/kbn-interpreter/tasks/build/paths.js
deleted file mode 100644
index a4cdba90a110a..0000000000000
--- a/packages/kbn-interpreter/tasks/build/paths.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 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.
- */
-
-const { resolve } = require('path');
-
-exports.ROOT_DIR = resolve(__dirname, '../../');
-exports.SOURCE_DIR = resolve(exports.ROOT_DIR, 'src');
-exports.BUILD_DIR = resolve(exports.ROOT_DIR, 'target');
-
-exports.BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset');
diff --git a/packages/kbn-interpreter/tsconfig.json b/packages/kbn-interpreter/tsconfig.json
index 3b81bbb118a55..011ed877146e8 100644
--- a/packages/kbn-interpreter/tsconfig.json
+++ b/packages/kbn-interpreter/tsconfig.json
@@ -1,7 +1,21 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
- "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-interpreter"
+ "allowJs": true,
+ "incremental": true,
+ "outDir": "./target",
+ "declaration": true,
+ "declarationMap": true,
+ "rootDir": "src",
+ "sourceMap": true,
+ "sourceRoot": "../../../../packages/kbn-interpreter/src",
+ "stripInternal": true,
+ "types": [
+ "jest",
+ "node"
+ ]
},
- "include": ["index.d.ts", "src/**/*.d.ts"]
+ "include": [
+ "src/**/*",
+ ]
}
diff --git a/packages/kbn-monaco/src/monaco_imports.ts b/packages/kbn-monaco/src/monaco_imports.ts
index 92ea23347c374..3f689e6ec0c01 100644
--- a/packages/kbn-monaco/src/monaco_imports.ts
+++ b/packages/kbn-monaco/src/monaco_imports.ts
@@ -7,7 +7,6 @@
*/
/* eslint-disable @kbn/eslint/module_migration */
-
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import 'monaco-editor/esm/vs/base/common/worker/simpleWorker';
@@ -23,4 +22,7 @@ import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover
import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature
import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching.js'; // Needed for brackets matching highlight
+import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'; // Needed for basic javascript support
+import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution.js'; // Needed for basic xml support
+
export { monaco };
diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel
new file mode 100644
index 0000000000000..3809c2b33d500
--- /dev/null
+++ b/packages/kbn-optimizer/BUILD.bazel
@@ -0,0 +1,120 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-optimizer"
+PKG_REQUIRE_NAME = "@kbn/optimizer"
+
+SOURCE_FILES = glob(
+ [
+ "src/**/*.ts",
+ ],
+ exclude = [
+ "**/*.test.*",
+ "**/__fixtures__/**",
+ "**/__snapshots__/**",
+ ],
+)
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "limits.yml",
+ "package.json",
+ "postcss.config.js",
+ "README.md"
+]
+
+SRC_DEPS = [
+ "//packages/kbn-config",
+ "//packages/kbn-dev-utils",
+ "//packages/kbn-std",
+ "//packages/kbn-ui-shared-deps",
+ "//packages/kbn-utils",
+ "@npm//chalk",
+ "@npm//clean-webpack-plugin",
+ "@npm//compression-webpack-plugin",
+ "@npm//cpy",
+ "@npm//del",
+ "@npm//execa",
+ "@npm//jest-diff",
+ "@npm//json-stable-stringify",
+ "@npm//lmdb-store",
+ "@npm//loader-utils",
+ "@npm//node-sass",
+ "@npm//normalize-path",
+ "@npm//pirates",
+ "@npm//resize-observer-polyfill",
+ "@npm//rxjs",
+ "@npm//source-map-support",
+ "@npm//watchpack",
+ "@npm//webpack",
+ "@npm//webpack-merge",
+ "@npm//webpack-sources",
+ "@npm//zlib"
+]
+
+TYPES_DEPS = [
+ "@npm//@types/compression-webpack-plugin",
+ "@npm//@types/jest",
+ "@npm//@types/json-stable-stringify",
+ "@npm//@types/loader-utils",
+ "@npm//@types/node",
+ "@npm//@types/normalize-path",
+ "@npm//@types/source-map-support",
+ "@npm//@types/watchpack",
+ "@npm//@types/webpack",
+ "@npm//@types/webpack-merge",
+ "@npm//@types/webpack-sources",
+]
+
+DEPS = SRC_DEPS + TYPES_DEPS
+
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//:tsconfig.base.json",
+ ],
+)
+
+ts_project(
+ name = "tsc",
+ args = ['--pretty'],
+ srcs = SRCS,
+ deps = DEPS,
+ declaration = True,
+ declaration_map = True,
+ incremental = True,
+ out_dir = "target",
+ source_map = True,
+ root_dir = "src",
+ tsconfig = ":tsconfig",
+)
+
+js_library(
+ name = PKG_BASE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES,
+ deps = DEPS + [":tsc"],
+ package_name = PKG_REQUIRE_NAME,
+ visibility = ["//visibility:public"],
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [
+ ":%s" % PKG_BASE_NAME,
+ ]
+)
+
+filegroup(
+ name = "build",
+ srcs = [
+ ":npm_module",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index 9adc075a7983f..c6960621359c7 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -34,7 +34,7 @@ pageLoadAssetSize:
indexManagement: 140608
indexPatternManagement: 28222
infra: 184320
- fleet: 450005
+ fleet: 465774
ingestPipelines: 58003
inputControlVis: 172675
inspector: 148711
@@ -67,7 +67,7 @@ pageLoadAssetSize:
searchprofiler: 67080
security: 95864
securityOss: 30806
- securitySolution: 76000
+ securitySolution: 217673
share: 99061
snapshotRestore: 79032
spaces: 57868
@@ -107,7 +107,7 @@ pageLoadAssetSize:
dataVisualizer: 27530
banners: 17946
mapsEms: 26072
- timelines: 28613
+ timelines: 230410
screenshotMode: 17856
visTypePie: 35583
cases: 144442
diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json
index a6c8284ad15f6..d23512f7c418d 100644
--- a/packages/kbn-optimizer/package.json
+++ b/packages/kbn-optimizer/package.json
@@ -4,10 +4,5 @@
"private": true,
"license": "SSPL-1.0 OR Elastic License 2.0",
"main": "./target/index.js",
- "types": "./target/index.d.ts",
- "scripts": {
- "build": "../../node_modules/.bin/tsc",
- "kbn:bootstrap": "yarn build",
- "kbn:watch": "yarn build --watch"
- }
+ "types": "./target/index.d.ts"
}
\ No newline at end of file
diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
index c175979f0e820..1f1e33d3dda7c 100644
--- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
+++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
@@ -123,7 +123,7 @@ exports[`prepares assets for distribution: metrics.json 1`] = `
\\"group\\": \\"page load bundle size\\",
\\"id\\": \\"foo\\",
\\"value\\": 4627,
- \\"limitConfigPath\\": \\"packages/kbn-optimizer/limits.yml\\"
+ \\"limitConfigPath\\": \\"node_modules/@kbn/optimizer/limits.yml\\"
},
{
\\"group\\": \\"async chunks size\\",
diff --git a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts
index 92875d3f69e46..d9e1bee22557b 100644
--- a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts
+++ b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts
@@ -79,7 +79,7 @@ export class BundleMetricsPlugin {
id: bundle.id,
value: entry.size,
limit: bundle.pageLoadAssetSizeLimit,
- limitConfigPath: `packages/kbn-optimizer/limits.yml`,
+ limitConfigPath: `node_modules/@kbn/optimizer/limits.yml`,
},
{
group: `async chunks size`,
diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json
index f2d508cf14a55..76beaf7689fd4 100644
--- a/packages/kbn-optimizer/tsconfig.json
+++ b/packages/kbn-optimizer/tsconfig.json
@@ -1,10 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
- "incremental": false,
+ "incremental": true,
"outDir": "./target",
"declaration": true,
"declarationMap": true,
+ "rootDir": "./src",
"sourceMap": true,
"sourceRoot": "../../../../packages/kbn-optimizer/src"
},
diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json
index 2d642d9ede13b..36a37075191a3 100644
--- a/packages/kbn-plugin-helpers/package.json
+++ b/packages/kbn-plugin-helpers/package.json
@@ -15,8 +15,5 @@
"scripts": {
"kbn:bootstrap": "rm -rf target && ../../node_modules/.bin/tsc",
"kbn:watch": "../../node_modules/.bin/tsc --watch"
- },
- "dependencies": {
- "@kbn/optimizer": "link:../kbn-optimizer"
}
}
\ No newline at end of file
diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js
index 1311eb4d7c638..5be9dff630ed5 100644
--- a/packages/kbn-pm/dist/index.js
+++ b/packages/kbn-pm/dist/index.js
@@ -48479,7 +48479,7 @@ async function runBazelCommandWithRunner(bazelCommandRunner, bazelArgs, offline
stdio: 'pipe'
});
- if (offline || !offline) {
+ if (offline) {
bazelArgs = [...bazelArgs, '--config=offline'];
}
@@ -63827,6 +63827,7 @@ function getProjectPaths({
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/plugin_functional/plugins/*'));
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/interpreter_functional/plugins/*'));
+ projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/server_integration/__fixtures__/plugins/*'));
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'examples/*'));
if (!ossOnly) {
diff --git a/packages/kbn-pm/src/config.ts b/packages/kbn-pm/src/config.ts
index a11b2ad9c72c3..666a2fed7a33c 100644
--- a/packages/kbn-pm/src/config.ts
+++ b/packages/kbn-pm/src/config.ts
@@ -31,6 +31,7 @@ export function getProjectPaths({ rootPath, ossOnly, skipKibanaPlugins }: Option
// correct and the expect behavior.
projectPaths.push(resolve(rootPath, 'test/plugin_functional/plugins/*'));
projectPaths.push(resolve(rootPath, 'test/interpreter_functional/plugins/*'));
+ projectPaths.push(resolve(rootPath, 'test/server_integration/__fixtures__/plugins/*'));
projectPaths.push(resolve(rootPath, 'examples/*'));
if (!ossOnly) {
diff --git a/packages/kbn-pm/src/utils/bazel/run.ts b/packages/kbn-pm/src/utils/bazel/run.ts
index 5f3743876e0e4..c030081e53daa 100644
--- a/packages/kbn-pm/src/utils/bazel/run.ts
+++ b/packages/kbn-pm/src/utils/bazel/run.ts
@@ -29,7 +29,7 @@ async function runBazelCommandWithRunner(
stdio: 'pipe',
};
- if (offline || !offline) {
+ if (offline) {
bazelArgs = [...bazelArgs, '--config=offline'];
}
diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts
index f75f0dcebf4f6..1909bcb1bcc2e 100644
--- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts
+++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts
@@ -42,6 +42,7 @@ export interface UseExceptionListsProps {
notifications: NotificationsStart;
pagination?: Pagination;
showTrustedApps: boolean;
+ showEventFilters: boolean;
}
export interface UseExceptionListProps {
diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts
index a9a93aa8df49a..0bd4c6c705668 100644
--- a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts
+++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts
@@ -28,6 +28,7 @@ export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination,
* @param namespaceTypes spaces to be searched
* @param notifications kibana service for displaying toasters
* @param showTrustedApps boolean - include/exclude trusted app lists
+ * @param showEventFilters boolean - include/exclude event filters lists
* @param pagination
*
*/
@@ -43,6 +44,7 @@ export const useExceptionLists = ({
namespaceTypes,
notifications,
showTrustedApps = false,
+ showEventFilters = false,
}: UseExceptionListsProps): ReturnExceptionLists => {
const [exceptionLists, setExceptionLists] = useState([]);
const [paginationInfo, setPagination] = useState(pagination);
@@ -51,8 +53,9 @@ export const useExceptionLists = ({
const namespaceTypesAsString = useMemo(() => namespaceTypes.join(','), [namespaceTypes]);
const filters = useMemo(
- (): string => getFilters(filterOptions, namespaceTypes, showTrustedApps),
- [namespaceTypes, filterOptions, showTrustedApps]
+ (): string =>
+ getFilters({ filters: filterOptions, namespaceTypes, showTrustedApps, showEventFilters }),
+ [namespaceTypes, filterOptions, showTrustedApps, showEventFilters]
);
useEffect(() => {
diff --git a/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts
new file mode 100644
index 0000000000000..934a9cbff56a6
--- /dev/null
+++ b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { getEventFiltersFilter } from '.';
+
+describe('getEventFiltersFilter', () => {
+ test('it returns filter to search for "exception-list" namespace trusted apps', () => {
+ const filter = getEventFiltersFilter(true, ['exception-list']);
+
+ expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_event_filters*)');
+ });
+
+ test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => {
+ const filter = getEventFiltersFilter(true, ['exception-list', 'exception-list-agnostic']);
+
+ expect(filter).toEqual(
+ '(exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it returns filter to exclude "exception-list" namespace trusted apps', () => {
+ const filter = getEventFiltersFilter(false, ['exception-list']);
+
+ expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_event_filters*)');
+ });
+
+ test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => {
+ const filter = getEventFiltersFilter(false, ['exception-list', 'exception-list-agnostic']);
+
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+});
diff --git a/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts
new file mode 100644
index 0000000000000..7e55073228fca
--- /dev/null
+++ b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants';
+import { SavedObjectType } from '../types';
+
+export const getEventFiltersFilter = (
+ showEventFilter: boolean,
+ namespaceTypes: SavedObjectType[]
+): string => {
+ if (showEventFilter) {
+ const filters = namespaceTypes.map((namespace) => {
+ return `${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`;
+ });
+ return `(${filters.join(' OR ')})`;
+ } else {
+ const filters = namespaceTypes.map((namespace) => {
+ return `not ${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`;
+ });
+ return `(${filters.join(' AND ')})`;
+ }
+};
diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts
index 327a29dc1b987..bfaad52ee8147 100644
--- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts
+++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts
@@ -11,106 +11,318 @@ import { getFilters } from '.';
describe('getFilters', () => {
describe('single', () => {
test('it properly formats when no filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters({}, ['single'], false);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
- expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)');
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
});
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters({}, ['single'], true);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
- expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)');
+ expect(filter).toEqual(
+ '(exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
});
test('it properly formats when filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], false);
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
);
});
test('it if filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], true);
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it if filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
expect(filter).toEqual(
- '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)'
);
});
});
describe('agnostic', () => {
test('it properly formats when no filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters({}, ['agnostic'], false);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters({}, ['agnostic'], true);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it properly formats when filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], false);
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it if filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], true);
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it if filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
expect(filter).toEqual(
- '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
});
describe('single, agnostic', () => {
test('it properly formats when no filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters({}, ['single', 'agnostic'], false);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters({}, ['single', 'agnostic'], true);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it properly formats when filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters(
- { created_by: 'moi', name: 'Sample' },
- ['single', 'agnostic'],
- false
- );
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it properly formats when filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters(
- { created_by: 'moi', name: 'Sample' },
- ['single', 'agnostic'],
- true
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
+ });
+
+ test('it properly formats when filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
expect(filter).toEqual(
- '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
});
diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts
index c9dd6ccae484c..238ae5541343c 100644
--- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts
+++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts
@@ -10,14 +10,26 @@ import { ExceptionListFilter, NamespaceType } from '@kbn/securitysolution-io-ts-
import { getGeneralFilters } from '../get_general_filters';
import { getSavedObjectTypes } from '../get_saved_object_types';
import { getTrustedAppsFilter } from '../get_trusted_apps_filter';
+import { getEventFiltersFilter } from '../get_event_filters_filter';
-export const getFilters = (
- filters: ExceptionListFilter,
- namespaceTypes: NamespaceType[],
- showTrustedApps: boolean
-): string => {
+export interface GetFiltersParams {
+ filters: ExceptionListFilter;
+ namespaceTypes: NamespaceType[];
+ showTrustedApps: boolean;
+ showEventFilters: boolean;
+}
+
+export const getFilters = ({
+ filters,
+ namespaceTypes,
+ showTrustedApps,
+ showEventFilters,
+}: GetFiltersParams): string => {
const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes });
const generalFilters = getGeneralFilters(filters, namespaces);
const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces);
- return [generalFilters, trustedAppsFilter].filter((filter) => filter.trim() !== '').join(' AND ');
+ const eventFiltersFilter = getEventFiltersFilter(showEventFilters, namespaces);
+ return [generalFilters, trustedAppsFilter, eventFiltersFilter]
+ .filter((filter) => filter.trim() !== '')
+ .join(' AND ');
};
diff --git a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts
index a483da152ac89..d208624b69fc5 100644
--- a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts
+++ b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts
@@ -95,6 +95,10 @@ export const filterExceptionItems = (
}
}, []);
+ if (entries.length === 0) {
+ return acc;
+ }
+
const item = { ...exception, entries };
if (exceptionListItemSchema.is(item)) {
diff --git a/packages/kbn-securitysolution-t-grid/BUILD.bazel b/packages/kbn-securitysolution-t-grid/BUILD.bazel
new file mode 100644
index 0000000000000..5cf1081bdd32e
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/BUILD.bazel
@@ -0,0 +1,125 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-securitysolution-t-grid"
+
+PKG_REQUIRE_NAME = "@kbn/securitysolution-t-grid"
+
+SOURCE_FILES = glob(
+ [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ ],
+ exclude = [
+ "**/*.test.*",
+ "**/*.mock.*",
+ ],
+)
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "react/package.json",
+ "package.json",
+ "README.md",
+]
+
+SRC_DEPS = [
+ "//packages/kbn-babel-preset",
+ "//packages/kbn-dev-utils",
+ "//packages/kbn-i18n",
+ "@npm//@babel/core",
+ "@npm//babel-loader",
+ "@npm//enzyme",
+ "@npm//jest",
+ "@npm//lodash",
+ "@npm//react",
+ "@npm//react-beautiful-dnd",
+ "@npm//tslib",
+]
+
+TYPES_DEPS = [
+ "@npm//typescript",
+ "@npm//@types/enzyme",
+ "@npm//@types/jest",
+ "@npm//@types/lodash",
+ "@npm//@types/node",
+ "@npm//@types/react",
+ "@npm//@types/react-beautiful-dnd",
+]
+
+DEPS = SRC_DEPS + TYPES_DEPS
+
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//:tsconfig.base.json",
+ ],
+)
+
+ts_config(
+ name = "tsconfig_browser",
+ src = "tsconfig.browser.json",
+ deps = [
+ "//:tsconfig.base.json",
+ "//:tsconfig.browser.json",
+ ],
+)
+
+ts_project(
+ name = "tsc",
+ args = ["--pretty"],
+ srcs = SRCS,
+ deps = DEPS,
+ declaration = True,
+ declaration_dir = "target_types",
+ declaration_map = True,
+ incremental = True,
+ out_dir = "target_node",
+ root_dir = "src",
+ source_map = True,
+ tsconfig = ":tsconfig",
+)
+
+ts_project(
+ name = "tsc_browser",
+ args = ['--pretty'],
+ srcs = SRCS,
+ deps = DEPS,
+ allow_js = True,
+ declaration = False,
+ incremental = True,
+ out_dir = "target_web",
+ source_map = True,
+ root_dir = "src",
+ tsconfig = ":tsconfig_browser",
+)
+
+js_library(
+ name = PKG_BASE_NAME,
+ package_name = PKG_REQUIRE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES,
+ visibility = ["//visibility:public"],
+ deps = [":tsc", ":tsc_browser"] + DEPS,
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [
+ ":%s" % PKG_BASE_NAME,
+ ],
+)
+
+filegroup(
+ name = "build",
+ srcs = [
+ ":npm_module",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-securitysolution-t-grid/README.md b/packages/kbn-securitysolution-t-grid/README.md
new file mode 100644
index 0000000000000..a49669c81689a
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/README.md
@@ -0,0 +1,3 @@
+# kbn-securitysolution-t-grid
+
+We do not want to create circular dependencies between security_solution and timelines plugins. Therefore , we will use this packages to share components between these two plugins.
diff --git a/packages/kbn-securitysolution-t-grid/babel.config.js b/packages/kbn-securitysolution-t-grid/babel.config.js
new file mode 100644
index 0000000000000..b4a118df51af5
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/babel.config.js
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ env: {
+ web: {
+ presets: ['@kbn/babel-preset/webpack_preset'],
+ },
+ node: {
+ presets: ['@kbn/babel-preset/node_preset'],
+ },
+ },
+ ignore: ['**/*.test.ts', '**/*.test.tsx'],
+};
diff --git a/packages/kbn-securitysolution-t-grid/jest.config.js b/packages/kbn-securitysolution-t-grid/jest.config.js
new file mode 100644
index 0000000000000..21e7d2d71b61a
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/jest.config.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../..',
+ roots: ['/packages/kbn-securitysolution-t-grid'],
+};
diff --git a/packages/kbn-securitysolution-t-grid/package.json b/packages/kbn-securitysolution-t-grid/package.json
new file mode 100644
index 0000000000000..68d3a8c71e7ca
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@kbn/securitysolution-t-grid",
+ "version": "1.0.0",
+ "description": "security solution t-grid packages will allow sharing components between timelines and security_solution plugin until we transfer all functionality to timelines plugin",
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "browser": "./target_web/browser.js",
+ "main": "./target_node/index.js",
+ "types": "./target_types/index.d.ts",
+ "private": true
+}
diff --git a/packages/kbn-securitysolution-t-grid/react/package.json b/packages/kbn-securitysolution-t-grid/react/package.json
new file mode 100644
index 0000000000000..c29ddd45f084d
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/react/package.json
@@ -0,0 +1,5 @@
+{
+ "browser": "../target_web/react",
+ "main": "../target_node/react",
+ "types": "../target_types/react/index.d.ts"
+}
\ No newline at end of file
diff --git a/packages/kbn-securitysolution-t-grid/src/constants/index.ts b/packages/kbn-securitysolution-t-grid/src/constants/index.ts
new file mode 100644
index 0000000000000..c03c0093d9839
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/constants/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export const HIGHLIGHTED_DROP_TARGET_CLASS_NAME = 'highlighted-drop-target';
+export const EMPTY_PROVIDERS_GROUP_CLASS_NAME = 'empty-providers-group';
+
+/** The draggable will move this many pixels via the keyboard when the arrow key is pressed */
+export const KEYBOARD_DRAG_OFFSET = 20;
+
+export const DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME = 'draggable-keyboard-wrapper';
+
+export const ROW_RENDERER_CLASS_NAME = 'row-renderer';
+
+export const NOTES_CONTAINER_CLASS_NAME = 'notes-container';
+
+export const NOTE_CONTENT_CLASS_NAME = 'note-content';
+
+/** This class is added to the document body while dragging */
+export const IS_DRAGGING_CLASS_NAME = 'is-dragging';
+
+export const HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME = 'hover-actions-always-show';
diff --git a/packages/kbn-storybook/typings.d.ts b/packages/kbn-securitysolution-t-grid/src/index.ts
similarity index 75%
rename from packages/kbn-storybook/typings.d.ts
rename to packages/kbn-securitysolution-t-grid/src/index.ts
index b940de2829909..0c2e9a7dbea8b 100644
--- a/packages/kbn-storybook/typings.d.ts
+++ b/packages/kbn-securitysolution-t-grid/src/index.ts
@@ -6,5 +6,6 @@
* Side Public License, v 1.
*/
-// Storybook react doesn't declare this in its typings, but it's there.
-declare module '@storybook/react/standalone';
+export * from './constants';
+export * from './utils';
+export * from './mock';
diff --git a/packages/kbn-interpreter/scripts/build.js b/packages/kbn-securitysolution-t-grid/src/mock/index.ts
similarity index 90%
rename from packages/kbn-interpreter/scripts/build.js
rename to packages/kbn-securitysolution-t-grid/src/mock/index.ts
index 21b7f86c6bc34..dc1b63dfc33b0 100644
--- a/packages/kbn-interpreter/scripts/build.js
+++ b/packages/kbn-securitysolution-t-grid/src/mock/index.ts
@@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
-require('../tasks/build/cli');
+export * from './mock_event_details';
diff --git a/x-pack/plugins/security_solution/common/utils/mock_event_details.ts b/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts
similarity index 97%
rename from x-pack/plugins/security_solution/common/utils/mock_event_details.ts
rename to packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts
index 7dc257ebb3fef..167fc9dd17a2a 100644
--- a/x-pack/plugins/security_solution/common/utils/mock_event_details.ts
+++ b/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
export const eventHit = {
diff --git a/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts
new file mode 100644
index 0000000000000..34e448419693b
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { has } from 'lodash/fp';
+
+export interface AppError extends Error {
+ body: {
+ message: string;
+ };
+}
+
+export interface KibanaError extends AppError {
+ body: {
+ message: string;
+ statusCode: number;
+ };
+}
+
+export interface SecurityAppError extends AppError {
+ body: {
+ message: string;
+ status_code: number;
+ };
+}
+
+export const isKibanaError = (error: unknown): error is KibanaError =>
+ has('message', error) && has('body.message', error) && has('body.statusCode', error);
+
+export const isSecurityAppError = (error: unknown): error is SecurityAppError =>
+ has('message', error) && has('body.message', error) && has('body.status_code', error);
+
+export const isAppError = (error: unknown): error is AppError =>
+ isKibanaError(error) || isSecurityAppError(error);
+
+export const isNotFoundError = (error: unknown) =>
+ (isKibanaError(error) && error.body.statusCode === 404) ||
+ (isSecurityAppError(error) && error.body.status_code === 404);
diff --git a/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts
new file mode 100644
index 0000000000000..91b2e88d97358
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts
@@ -0,0 +1,133 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { DropResult } from 'react-beautiful-dnd';
+
+export const draggableIdPrefix = 'draggableId';
+
+export const droppableIdPrefix = 'droppableId';
+
+export const draggableContentPrefix = `${draggableIdPrefix}.content.`;
+
+export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`;
+
+export const draggableFieldPrefix = `${draggableIdPrefix}.field.`;
+
+export const droppableContentPrefix = `${droppableIdPrefix}.content.`;
+
+export const droppableFieldPrefix = `${droppableIdPrefix}.field.`;
+
+export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`;
+
+export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`;
+
+export const droppableTimelineFlyoutBottomBarPrefix = `${droppableIdPrefix}.flyoutButton.`;
+
+export const getDraggableId = (dataProviderId: string): string =>
+ `${draggableContentPrefix}${dataProviderId}`;
+
+export const getDraggableFieldId = ({
+ contextId,
+ fieldId,
+}: {
+ contextId: string;
+ fieldId: string;
+}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`;
+
+export const getTimelineProviderDroppableId = ({
+ groupIndex,
+ timelineId,
+}: {
+ groupIndex: number;
+ timelineId: string;
+}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`;
+
+export const getTimelineProviderDraggableId = ({
+ dataProviderId,
+ groupIndex,
+ timelineId,
+}: {
+ dataProviderId: string;
+ groupIndex: number;
+ timelineId: string;
+}): string =>
+ `${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`;
+
+export const getDroppableId = (visualizationPlaceholderId: string): string =>
+ `${droppableContentPrefix}${visualizationPlaceholderId}`;
+
+export const sourceIsContent = (result: DropResult): boolean =>
+ result.source.droppableId.startsWith(droppableContentPrefix);
+
+export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => {
+ const regex = /^droppableId\.timelineProviders\.(\S+)\./;
+ const sourceMatches = result.source.droppableId.match(regex) || [];
+ const destinationMatches =
+ (result.destination && result.destination.droppableId.match(regex)) || [];
+
+ return (
+ sourceMatches.length >= 2 &&
+ destinationMatches.length >= 2 &&
+ sourceMatches[1] === destinationMatches[1]
+ );
+};
+
+export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean =>
+ result.draggableId.startsWith(draggableContentPrefix);
+
+export const draggableIsField = (result: DropResult | { draggableId: string }): boolean =>
+ result.draggableId.startsWith(draggableFieldPrefix);
+
+export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP';
+
+export const destinationIsTimelineProviders = (result: DropResult): boolean =>
+ result.destination != null &&
+ result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix);
+
+export const destinationIsTimelineColumns = (result: DropResult): boolean =>
+ result.destination != null &&
+ result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix);
+
+export const destinationIsTimelineButton = (result: DropResult): boolean =>
+ result.destination != null &&
+ result.destination.droppableId.startsWith(droppableTimelineFlyoutBottomBarPrefix);
+
+export const getProviderIdFromDraggable = (result: DropResult): string =>
+ result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1);
+
+export const getFieldIdFromDraggable = (result: DropResult): string =>
+ unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1));
+
+export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_');
+
+export const escapeContextId = (path: string) => path.replace(/\./g, '_');
+
+export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!');
+
+export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.');
+
+export const providerWasDroppedOnTimeline = (result: DropResult): boolean =>
+ reasonIsDrop(result) &&
+ draggableIsContent(result) &&
+ sourceIsContent(result) &&
+ destinationIsTimelineProviders(result);
+
+export const userIsReArrangingProviders = (result: DropResult): boolean =>
+ reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result);
+
+export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean =>
+ reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result);
+
+/**
+ * Prevents fields from being dragged or dropped to any area other than column
+ * header drop zone in the timeline
+ */
+export const DRAG_TYPE_FIELD = 'drag-type-field';
+
+/** This class is added to the document body while timeline field dragging */
+export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging';
diff --git a/packages/kbn-securitysolution-t-grid/src/utils/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/index.ts
new file mode 100644
index 0000000000000..39629a990c539
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/utils/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './api';
+export * from './drag_and_drop';
diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.browser.json b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json
new file mode 100644
index 0000000000000..a5183ba4fd457
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../tsconfig.browser.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "incremental": true,
+ "outDir": "./target_web",
+ "declaration": false,
+ "isolatedModules": true,
+ "sourceMap": true,
+ "sourceRoot": "../../../../../packages/kbn-securitysolution-t-grid/src",
+ "types": [
+ "jest",
+ "node"
+ ],
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ ],
+ "exclude": [
+ "**/__fixtures__/**/*"
+ ]
+}
diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.json b/packages/kbn-securitysolution-t-grid/tsconfig.json
new file mode 100644
index 0000000000000..8cda578edede4
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "incremental": true,
+ "outDir": "target",
+ "rootDir": "src",
+ "sourceMap": true,
+ "sourceRoot": "../../../../packages/kbn-securitysolution-t-grid/src",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "src/**/*"
+ ]
+}
diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel
new file mode 100644
index 0000000000000..e18256aeb8da4
--- /dev/null
+++ b/packages/kbn-storybook/BUILD.bazel
@@ -0,0 +1,98 @@
+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-storybook"
+PKG_REQUIRE_NAME = "@kbn/storybook"
+
+SOURCE_FILES = glob(
+ [
+ "lib/**/*.ts",
+ "lib/**/*.tsx",
+ "*.ts",
+ ],
+ exclude = ["**/*.test.*"],
+)
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "preset/package.json",
+ "templates/index.ejs",
+ "package.json",
+ "README.md",
+ "preset.js",
+]
+
+SRC_DEPS = [
+ "//packages/kbn-dev-utils",
+ "//packages/kbn-ui-shared-deps",
+ "@npm//@storybook/addons",
+ "@npm//@storybook/api",
+ "@npm//@storybook/components",
+ "@npm//@storybook/core",
+ "@npm//@storybook/node-logger",
+ "@npm//@storybook/react",
+ "@npm//@storybook/theming",
+ "@npm//loader-utils",
+ "@npm//react",
+ "@npm//webpack",
+ "@npm//webpack-merge",
+]
+
+TYPES_DEPS = [
+ "@npm//@types/loader-utils",
+ "@npm//@types/node",
+ "@npm//@types/webpack",
+ "@npm//@types/webpack-merge",
+]
+
+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,
+ tsconfig = ":tsconfig",
+)
+
+js_library(
+ name = PKG_BASE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES,
+ deps = [":tsc"] + 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/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json
index f2e4c9b3418b1..f3c12f19a0793 100644
--- a/packages/kbn-storybook/package.json
+++ b/packages/kbn-storybook/package.json
@@ -7,10 +7,5 @@
"types": "./target/index.d.ts",
"kibana": {
"devOnly": true
- },
- "scripts": {
- "build": "../../node_modules/.bin/tsc",
- "kbn:bootstrap": "yarn build",
- "kbn:watch": "yarn build --watch"
}
}
\ No newline at end of file
diff --git a/packages/kbn-storybook/preset.js b/packages/kbn-storybook/preset.js
index c1b7195c141b4..be0012a3818b1 100644
--- a/packages/kbn-storybook/preset.js
+++ b/packages/kbn-storybook/preset.js
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+// eslint-disable-next-line
const webpackConfig = require('./target/webpack.config').default;
module.exports = {
diff --git a/packages/kbn-storybook/preset/package.json b/packages/kbn-storybook/preset/package.json
new file mode 100644
index 0000000000000..7cd7517d64dde
--- /dev/null
+++ b/packages/kbn-storybook/preset/package.json
@@ -0,0 +1,4 @@
+{
+ "private": true,
+ "main": "../preset.js"
+}
\ No newline at end of file
diff --git a/packages/kbn-storybook/lib/templates/index.ejs b/packages/kbn-storybook/templates/index.ejs
similarity index 100%
rename from packages/kbn-storybook/lib/templates/index.ejs
rename to packages/kbn-storybook/templates/index.ejs
diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json
index 586f5ea32c056..1f6886c45c505 100644
--- a/packages/kbn-storybook/tsconfig.json
+++ b/packages/kbn-storybook/tsconfig.json
@@ -1,14 +1,15 @@
{
- "extends": "../../tsconfig.json",
+ "extends": "../../tsconfig.base.json",
"compilerOptions": {
- "incremental": false,
+ "incremental": true,
"outDir": "target",
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"sourceRoot": "../../../../packages/kbn-storybook",
+ "target": "es2015",
"types": ["node"]
},
- "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx", "../../typings/index.d.ts"]
+ "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx"]
}
diff --git a/packages/kbn-storybook/typings.ts b/packages/kbn-storybook/typings.ts
new file mode 100644
index 0000000000000..6c5d8f4da5709
--- /dev/null
+++ b/packages/kbn-storybook/typings.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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.
+ */
+
+// Storybook react doesn't declare this in its typings, but it's there.
+declare module '@storybook/react/standalone';
+
+// Storybook references this module. It's @ts-ignored in the codebase but when
+// built into its dist it strips that out. Add it here to avoid a type checking
+// error.
+//
+// See https://github.com/storybookjs/storybook/issues/11684
+declare module 'react-syntax-highlighter/dist/cjs/create-element';
+declare module 'react-syntax-highlighter/dist/cjs/prism-light';
diff --git a/packages/kbn-storybook/webpack.config.ts b/packages/kbn-storybook/webpack.config.ts
index 972caf8d481fe..41d3ee1f7ee5c 100644
--- a/packages/kbn-storybook/webpack.config.ts
+++ b/packages/kbn-storybook/webpack.config.ts
@@ -94,7 +94,7 @@ export default function ({ config: storybookConfig }: { config: Configuration })
return plugin.options && typeof plugin.options.template === 'string';
});
if (htmlWebpackPlugin) {
- htmlWebpackPlugin.options.template = require.resolve('../lib/templates/index.ejs');
+ htmlWebpackPlugin.options.template = require.resolve('../templates/index.ejs');
}
return webpackMerge(storybookConfig, config);
diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js
index 225f93d487823..5baff607704c7 100644
--- a/packages/kbn-test/jest-preset.js
+++ b/packages/kbn-test/jest-preset.js
@@ -94,7 +94,7 @@ module.exports = {
transformIgnorePatterns: [
// ignore all node_modules except monaco-editor and react-monaco-editor which requires babel transforms to handle dynamic import()
// since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842)
- '[/\\\\]node_modules(?![\\/\\\\](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 275d9fac73c58..aaff513f1591f 100644
--- a/packages/kbn-test/package.json
+++ b/packages/kbn-test/package.json
@@ -12,8 +12,5 @@
},
"kibana": {
"devOnly": true
- },
- "dependencies": {
- "@kbn/optimizer": "link:../kbn-optimizer"
}
}
\ No newline at end of file
diff --git a/packages/kbn-test/src/functional_tests/lib/auth.ts b/packages/kbn-test/src/functional_tests/lib/auth.ts
deleted file mode 100644
index abd1e0f9e7d5e..0000000000000
--- a/packages/kbn-test/src/functional_tests/lib/auth.ts
+++ /dev/null
@@ -1,188 +0,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 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import fs from 'fs';
-import util from 'util';
-import { format as formatUrl } from 'url';
-import request from 'request';
-import type { ToolingLog } from '@kbn/dev-utils';
-
-export const DEFAULT_SUPERUSER_PASS = 'changeme';
-const readFile = util.promisify(fs.readFile);
-
-function delay(delayMs: number) {
- return new Promise((res) => setTimeout(res, delayMs));
-}
-
-interface UpdateCredentialsOptions {
- port: number;
- auth: string;
- username: string;
- password: string;
- retries?: number;
- protocol: string;
- caCert?: Buffer | string;
-}
-async function updateCredentials({
- port,
- auth,
- username,
- password,
- retries = 10,
- protocol,
- caCert,
-}: UpdateCredentialsOptions): Promise {
- const result = await new Promise<{ body: any; httpResponse: request.Response }>(
- (resolve, reject) =>
- request(
- {
- method: 'PUT',
- uri: formatUrl({
- protocol: `${protocol}:`,
- auth,
- hostname: 'localhost',
- port,
- pathname: `/_security/user/${username}/_password`,
- }),
- json: true,
- body: { password },
- ca: caCert,
- },
- (err, httpResponse, body) => {
- if (err) return reject(err);
- resolve({ httpResponse, body });
- }
- )
- );
-
- const { body, httpResponse } = result;
- const { statusCode } = httpResponse;
-
- if (statusCode === 200) {
- return;
- }
-
- if (retries > 0) {
- await delay(2500);
- return await updateCredentials({
- port,
- auth,
- username,
- password,
- retries: retries - 1,
- protocol,
- caCert,
- });
- }
-
- throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`);
-}
-
-interface SetupUsersOptions {
- log: ToolingLog;
- esPort: number;
- updates: Array<{ username: string; password: string; roles?: string[] }>;
- protocol?: string;
- caPath?: string;
-}
-
-export async function setupUsers({
- log,
- esPort,
- updates,
- protocol = 'http',
- caPath,
-}: SetupUsersOptions): Promise {
- // track the current credentials for the `elastic` user as
- // they will likely change as we apply updates
- let auth = `elastic:${DEFAULT_SUPERUSER_PASS}`;
- const caCert = caPath ? await readFile(caPath) : undefined;
-
- for (const { username, password, roles } of updates) {
- // If working with a built-in user, just change the password
- if (['logstash_system', 'elastic', 'kibana'].includes(username)) {
- await updateCredentials({ port: esPort, auth, username, password, protocol, caCert });
- log.info('setting %j user password to %j', username, password);
-
- // If not a builtin user, add them
- } else {
- await insertUser({ port: esPort, auth, username, password, roles, protocol, caCert });
- log.info('Added %j user with password to %j', username, password);
- }
-
- if (username === 'elastic') {
- auth = `elastic:${password}`;
- }
- }
-}
-
-interface InserUserOptions {
- port: number;
- auth: string;
- username: string;
- password: string;
- roles?: string[];
- retries?: number;
- protocol: string;
- caCert?: Buffer | string;
-}
-async function insertUser({
- port,
- auth,
- username,
- password,
- roles = [],
- retries = 10,
- protocol,
- caCert,
-}: InserUserOptions): Promise {
- const result = await new Promise<{ body: any; httpResponse: request.Response }>(
- (resolve, reject) =>
- request(
- {
- method: 'POST',
- uri: formatUrl({
- protocol: `${protocol}:`,
- auth,
- hostname: 'localhost',
- port,
- pathname: `/_security/user/${username}`,
- }),
- json: true,
- body: { password, roles },
- ca: caCert,
- },
- (err, httpResponse, body) => {
- if (err) return reject(err);
- resolve({ httpResponse, body });
- }
- )
- );
-
- const { body, httpResponse } = result;
- const { statusCode } = httpResponse;
- if (statusCode === 200) {
- return;
- }
-
- if (retries > 0) {
- await delay(2500);
- return await insertUser({
- port,
- auth,
- username,
- password,
- roles,
- retries: retries - 1,
- protocol,
- caCert,
- });
- }
-
- throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`);
-}
diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
index 7ba9a3c1c4733..da83d8285a6b5 100644
--- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
+++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
@@ -12,8 +12,6 @@ import { KIBANA_ROOT } from './paths';
import type { Config } from '../../functional_test_runner/';
import { createTestEsCluster } from '../../es';
-import { setupUsers, DEFAULT_SUPERUSER_PASS } from './auth';
-
interface RunElasticsearchOptions {
log: ToolingLog;
esFrom: string;
@@ -34,9 +32,7 @@ export async function runElasticsearch({
const cluster = createTestEsCluster({
port: config.get('servers.elasticsearch.port'),
- password: isSecurityEnabled
- ? DEFAULT_SUPERUSER_PASS
- : config.get('servers.elasticsearch.password'),
+ password: isSecurityEnabled ? 'changeme' : config.get('servers.elasticsearch.password'),
license,
log,
basePath: resolve(KIBANA_ROOT, '.es'),
@@ -49,22 +45,5 @@ export async function runElasticsearch({
await cluster.start();
- if (isSecurityEnabled) {
- await setupUsers({
- log,
- esPort: config.get('servers.elasticsearch.port'),
- updates: [config.get('servers.elasticsearch'), config.get('servers.kibana')],
- protocol: config.get('servers.elasticsearch').protocol,
- caPath: getRelativeCertificateAuthorityPath(config.get('kbnTestServer.serverArgs')),
- });
- }
-
return cluster;
}
-
-function getRelativeCertificateAuthorityPath(esConfig: string[] = []) {
- const caConfig = esConfig.find(
- (config) => config.indexOf('--elasticsearch.ssl.certificateAuthorities') === 0
- );
- return caConfig ? caConfig.split('=')[1] : undefined;
-}
diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts
index dd5343b0118b3..af100a33ea3a7 100644
--- a/packages/kbn-test/src/index.ts
+++ b/packages/kbn-test/src/index.ts
@@ -29,8 +29,6 @@ export { esTestConfig, createTestEsCluster } from './es';
export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn';
-export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth';
-
export { readConfigFile } from './functional_test_runner/lib/config/read_config_file';
export { runFtrCli } from './functional_test_runner/cli';
diff --git a/packages/kbn-test/src/jest/setup/babel_polyfill.js b/packages/kbn-test/src/jest/setup/babel_polyfill.js
index d112e4d4fcb39..7dda4cceec65c 100644
--- a/packages/kbn-test/src/jest/setup/babel_polyfill.js
+++ b/packages/kbn-test/src/jest/setup/babel_polyfill.js
@@ -9,4 +9,4 @@
// Note: In theory importing the polyfill should not be needed, as Babel should
// include the necessary polyfills when using `@babel/preset-env`, but for some
// reason it did not work. See https://github.com/elastic/kibana/issues/14506
-import '@kbn/optimizer/src/node/polyfill';
+import '@kbn/optimizer/target/node/polyfill';
diff --git a/packages/kbn-tinymath/grammar/grammar.peggy b/packages/kbn-tinymath/grammar/grammar.peggy
index 1c6f8c3334c23..414bc2fa11cb7 100644
--- a/packages/kbn-tinymath/grammar/grammar.peggy
+++ b/packages/kbn-tinymath/grammar/grammar.peggy
@@ -43,7 +43,7 @@ Literal "literal"
// Quoted variables are interpreted as strings
// but unquoted variables are more restrictive
Variable
- = _ [\'] chars:(ValidChar / Space / [\"])* [\'] _ {
+ = _ '"' chars:("\\\"" { return "\""; } / [^"])* '"' _ {
return {
type: 'variable',
value: chars.join(''),
@@ -51,7 +51,7 @@ Variable
text: text()
};
}
- / _ [\"] chars:(ValidChar / Space / [\'])* [\"] _ {
+ / _ "'" chars:("\\\'" { return "\'"; } / [^'])* "'" _ {
return {
type: 'variable',
value: chars.join(''),
diff --git a/packages/kbn-tinymath/test/library.test.js b/packages/kbn-tinymath/test/library.test.js
index bbc8503684fd4..9d87919c4f1ac 100644
--- a/packages/kbn-tinymath/test/library.test.js
+++ b/packages/kbn-tinymath/test/library.test.js
@@ -92,6 +92,7 @@ describe('Parser', () => {
expect(parse('@foo0')).toEqual(variableEqual('@foo0'));
expect(parse('.foo0')).toEqual(variableEqual('.foo0'));
expect(parse('-foo0')).toEqual(variableEqual('-foo0'));
+ expect(() => parse(`foo😀\t')`)).toThrow('Failed to parse');
});
});
@@ -103,6 +104,7 @@ describe('Parser', () => {
expect(parse('"foo bar fizz buzz"')).toEqual(variableEqual('foo bar fizz buzz'));
expect(parse('"foo bar baby"')).toEqual(variableEqual('foo bar baby'));
expect(parse(`"f'oo"`)).toEqual(variableEqual(`f'oo`));
+ expect(parse(`"foo😀\t"`)).toEqual(variableEqual(`foo😀\t`));
});
it('strings with single quotes', () => {
@@ -119,6 +121,7 @@ describe('Parser', () => {
expect(parse("'foo bar '")).toEqual(variableEqual("foo bar "));
expect(parse("'0foo'")).toEqual(variableEqual("0foo"));
expect(parse(`'f"oo'`)).toEqual(variableEqual(`f"oo`));
+ expect(parse(`'foo😀\t'`)).toEqual(variableEqual(`foo😀\t`));
/* eslint-enable prettier/prettier */
});
diff --git a/packages/kbn-ui-framework/BUILD.bazel b/packages/kbn-ui-framework/BUILD.bazel
new file mode 100644
index 0000000000000..f8cf5035bdc5f
--- /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/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js
index 438b1e0b2e77b..9d18c8033ff67 100644
--- a/packages/kbn-ui-shared-deps/webpack.config.js
+++ b/packages/kbn-ui-shared-deps/webpack.config.js
@@ -7,7 +7,6 @@
*/
const Path = require('path');
-const Os = require('os');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
@@ -31,7 +30,8 @@ module.exports = {
'kbn-ui-shared-deps.v8.light': ['@elastic/eui/dist/eui_theme_amsterdam_light.css'],
},
context: __dirname,
- devtool: 'cheap-source-map',
+ // cheap-source-map should be used if needed
+ devtool: false,
output: {
path: UiSharedDeps.distDir,
filename: '[name].js',
@@ -39,7 +39,6 @@ module.exports = {
devtoolModuleFilenameTemplate: (info) =>
`kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`,
library: '__kbnSharedDeps__',
- futureEmitAssets: true,
},
module: {
@@ -111,7 +110,7 @@ module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin({
- parallel: Math.min(Os.cpus().length, 2),
+ parallel: false,
minimizerOptions: {
preset: [
'default',
@@ -125,7 +124,7 @@ module.exports = {
cache: false,
sourceMap: false,
extractComments: false,
- parallel: Math.min(Os.cpus().length, 2),
+ parallel: false,
terserOptions: {
compress: true,
mangle: true,
diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts
index 32fc330375991..f5af7011e632e 100644
--- a/src/core/public/apm_system.ts
+++ b/src/core/public/apm_system.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import type { ApmBase } from '@elastic/apm-rum';
+import type { ApmBase, AgentConfigOptions } from '@elastic/apm-rum';
import { modifyUrl } from '@kbn/std';
import type { InternalApplicationStart } from './application';
@@ -18,9 +18,8 @@ const HTTP_REQUEST_TRANSACTION_NAME_REGEX = /^(GET|POST|PUT|HEAD|PATCH|DELETE|OP
* that lives in the Kibana Platform.
*/
-interface ApmConfig {
- // AgentConfigOptions is not exported from @elastic/apm-rum
- active?: boolean;
+interface ApmConfig extends AgentConfigOptions {
+ // Kibana-specific config settings:
globalLabels?: Record;
}
diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts
index 3ed164088bf5c..de9e4d4496f3b 100644
--- a/src/core/public/application/application_service.test.ts
+++ b/src/core/public/application/application_service.test.ts
@@ -15,13 +15,13 @@ import {
import { createElement } from 'react';
import { BehaviorSubject, Subject } from 'rxjs';
import { bufferCount, take, takeUntil } from 'rxjs/operators';
-import { shallow, mount } from 'enzyme';
+import { mount, shallow } from 'enzyme';
import { httpServiceMock } from '../http/http_service.mock';
import { overlayServiceMock } from '../overlays/overlay_service.mock';
import { MockLifecycle } from './test_types';
import { ApplicationService } from './application_service';
-import { App, PublicAppInfo, AppNavLinkStatus, AppStatus, AppUpdater } from './types';
+import { App, AppDeepLink, AppNavLinkStatus, AppStatus, AppUpdater, PublicAppInfo } from './types';
import { act } from 'react-dom/test-utils';
const createApp = (props: Partial): App => {
@@ -365,6 +365,85 @@ describe('#setup()', () => {
expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined);
MockHistory.push.mockClear();
});
+
+ it('preserves the deep links if the update does not modify them', async () => {
+ const setup = service.setup(setupDeps);
+
+ const pluginId = Symbol('plugin');
+ const updater$ = new BehaviorSubject((app) => ({}));
+
+ const deepLinks: AppDeepLink[] = [
+ {
+ id: 'foo',
+ title: 'Foo',
+ searchable: true,
+ navLinkStatus: AppNavLinkStatus.visible,
+ path: '/foo',
+ },
+ {
+ id: 'bar',
+ title: 'Bar',
+ searchable: false,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ path: '/bar',
+ },
+ ];
+
+ setup.register(pluginId, createApp({ id: 'app1', deepLinks, updater$ }));
+
+ const { applications$ } = await service.start(startDeps);
+
+ updater$.next((app) => ({ defaultPath: '/foo' }));
+
+ let appInfos = await applications$.pipe(take(1)).toPromise();
+
+ expect(appInfos.get('app1')!.deepLinks).toEqual([
+ {
+ deepLinks: [],
+ id: 'foo',
+ keywords: [],
+ navLinkStatus: 1,
+ path: '/foo',
+ searchable: true,
+ title: 'Foo',
+ },
+ {
+ deepLinks: [],
+ id: 'bar',
+ keywords: [],
+ navLinkStatus: 3,
+ path: '/bar',
+ searchable: false,
+ title: 'Bar',
+ },
+ ]);
+
+ updater$.next((app) => ({
+ deepLinks: [
+ {
+ id: 'bar',
+ title: 'Bar',
+ searchable: false,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ path: '/bar',
+ },
+ ],
+ }));
+
+ appInfos = await applications$.pipe(take(1)).toPromise();
+
+ expect(appInfos.get('app1')!.deepLinks).toEqual([
+ {
+ deepLinks: [],
+ id: 'bar',
+ keywords: [],
+ navLinkStatus: 3,
+ path: '/bar',
+ searchable: false,
+ title: 'Bar',
+ },
+ ]);
+ });
});
});
diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx
index 8c6090caabce1..2e804bf2f5413 100644
--- a/src/core/public/application/application_service.tsx
+++ b/src/core/public/application/application_service.tsx
@@ -54,6 +54,7 @@ function filterAvailable(m: Map, capabilities: Capabilities) {
)
);
}
+
const findMounter = (mounters: Map, appRoute?: string) =>
[...mounters].find(([, mounter]) => mounter.appRoute === appRoute);
@@ -414,13 +415,11 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => {
changes.navLinkStatus ?? AppNavLinkStatus.default,
fields.navLinkStatus ?? AppNavLinkStatus.default
),
- // deepLinks take the last defined update
- deepLinks: fields.deepLinks
- ? populateDeepLinkDefaults(fields.deepLinks)
- : changes.deepLinks,
+ ...(fields.deepLinks ? { deepLinks: populateDeepLinkDefaults(fields.deepLinks) } : {}),
};
}
});
+
return {
...app,
...changes,
diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts
index fa1e2dd9a4537..25614d1d1dca9 100644
--- a/src/core/public/application/utils/get_app_info.test.ts
+++ b/src/core/public/application/utils/get_app_info.test.ts
@@ -185,15 +185,18 @@ describe('getAppInfo', () => {
it('adds default deepLinks when needed', () => {
const app = createApp({
+ order: 3,
deepLinks: [
{
id: 'sub-id',
title: 'sub-title',
+ order: 2,
deepLinks: [
{
id: 'sub-sub-id',
title: 'sub-sub-title',
path: '/sub-sub',
+ order: 1,
keywords: ['sub sub'],
},
],
@@ -210,12 +213,14 @@ describe('getAppInfo', () => {
searchable: true,
appRoute: `/app/some-id`,
keywords: [],
+ order: 3,
deepLinks: [
{
id: 'sub-id',
title: 'sub-title',
navLinkStatus: AppNavLinkStatus.hidden,
searchable: true,
+ order: 2,
keywords: [],
deepLinks: [
{
@@ -223,6 +228,7 @@ describe('getAppInfo', () => {
title: 'sub-sub-title',
navLinkStatus: AppNavLinkStatus.hidden,
searchable: true,
+ order: 1,
path: '/sub-sub',
keywords: ['sub sub'],
deepLinks: [],
diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts
index 6c753b7a71a0f..b5a3f0b0a0f13 100644
--- a/src/core/public/application/utils/get_app_info.ts
+++ b/src/core/public/application/utils/get_app_info.ts
@@ -41,9 +41,7 @@ function getDeepLinkInfos(deepLinks?: AppDeepLink[]): PublicAppDeepLinkInfo[] {
return deepLinks.map(
({ navLinkStatus = AppNavLinkStatus.default, ...rawDeepLink }): PublicAppDeepLinkInfo => {
return {
- id: rawDeepLink.id,
- title: rawDeepLink.title,
- path: rawDeepLink.path,
+ ...rawDeepLink,
keywords: rawDeepLink.keywords ?? [],
navLinkStatus:
navLinkStatus === AppNavLinkStatus.default ? AppNavLinkStatus.hidden : navLinkStatus,
diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts
index 0264c8a1acf75..92f5a854f6b00 100644
--- a/src/core/public/chrome/chrome_service.test.ts
+++ b/src/core/public/chrome/chrome_service.test.ts
@@ -53,8 +53,21 @@ function defaultStartDeps(availableApps?: App[]) {
return deps;
}
+function defaultStartTestOptions({
+ browserSupportsCsp = true,
+ kibanaVersion = 'version',
+}: {
+ browserSupportsCsp?: boolean;
+ kibanaVersion?: string;
+}): any {
+ return {
+ browserSupportsCsp,
+ kibanaVersion,
+ };
+}
+
async function start({
- options = { browserSupportsCsp: true },
+ options = defaultStartTestOptions({}),
cspConfigMock = { warnLegacyBrowsers: true },
startDeps = defaultStartDeps(),
}: { options?: any; cspConfigMock?: any; startDeps?: ReturnType } = {}) {
@@ -82,7 +95,9 @@ afterAll(() => {
describe('start', () => {
it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', async () => {
- const { startDeps } = await start({ options: { browserSupportsCsp: false } });
+ const { startDeps } = await start({
+ options: { browserSupportsCsp: false, kibanaVersion: '7.0.0' },
+ });
expect(startDeps.notifications.toasts.addWarning.mock.calls).toMatchInlineSnapshot(`
Array [
@@ -95,6 +110,41 @@ describe('start', () => {
`);
});
+ it('adds the kibana versioned class to the document body', async () => {
+ const { chrome, service } = await start({
+ options: { browserSupportsCsp: false, kibanaVersion: '1.2.3' },
+ });
+ const promise = chrome.getBodyClasses$().pipe(toArray()).toPromise();
+ service.stop();
+ await expect(promise).resolves.toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "kbnBody",
+ "kbnBody--noHeaderBanner",
+ "kbnBody--chromeHidden",
+ "kbnVersion-1-2-3",
+ ],
+ ]
+ `);
+ });
+ it('strips off "snapshot" from the kibana version if present', async () => {
+ const { chrome, service } = await start({
+ options: { browserSupportsCsp: false, kibanaVersion: '8.0.0-SnAPshot' },
+ });
+ const promise = chrome.getBodyClasses$().pipe(toArray()).toPromise();
+ service.stop();
+ await expect(promise).resolves.toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "kbnBody",
+ "kbnBody--noHeaderBanner",
+ "kbnBody--chromeHidden",
+ "kbnVersion-8-0-0",
+ ],
+ ]
+ `);
+ });
+
it('does not add legacy browser warning if browser supports CSP', async () => {
const { startDeps } = await start();
diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx
index 5ed447edde75a..f1381c52ce779 100644
--- a/src/core/public/chrome/chrome_service.tsx
+++ b/src/core/public/chrome/chrome_service.tsx
@@ -37,9 +37,11 @@ import {
export type { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle };
const IS_LOCKED_KEY = 'core.chrome.isLocked';
+const SNAPSHOT_REGEX = /-snapshot/i;
interface ConstructorParams {
browserSupportsCsp: boolean;
+ kibanaVersion: string;
}
interface StartDeps {
@@ -116,6 +118,16 @@ export class ChromeService {
const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK);
const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true');
+ const getKbnVersionClass = () => {
+ // we assume that the version is valid and has the form 'X.X.X'
+ // strip out `SNAPSHOT` and reformat to 'X-X-X'
+ const formattedVersionClass = this.params.kibanaVersion
+ .replace(SNAPSHOT_REGEX, '')
+ .split('.')
+ .join('-');
+ return `kbnVersion-${formattedVersionClass}`;
+ };
+
const headerBanner$ = new BehaviorSubject(undefined);
const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!]).pipe(
map(([headerBanner, isVisible]) => {
@@ -123,6 +135,7 @@ export class ChromeService {
'kbnBody',
headerBanner ? 'kbnBody--hasHeaderBanner' : 'kbnBody--noHeaderBanner',
isVisible ? 'kbnBody--chromeVisible' : 'kbnBody--chromeHidden',
+ getKbnVersionClass(),
];
})
);
diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
index 3668829a6888c..0b10209bc13e5 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
@@ -370,54 +370,62 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
isOpen={true}
onClose={[Function]}
>
-
-
-
- }
- />
-
-
-
-
+
-
-
-
-
+ Custom link
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+ data-euiicon-type="home"
+ />
+
+
+ Home
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+ Recently viewed
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-recentlyViewed"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="recentlyViewed"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Recently viewed"
+ paddingSize="none"
>
-
-
-
-
- Recently viewed
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
-
-
- Recently viewed
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+ Analytics
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-kibana"
- iconType="logoKibana"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="kibana"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Analytics"
+ paddingSize="none"
>
-
-
-
-
-
-
-
- Analytics
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
-
-
- Analytics
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+ dashboard
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Observability
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-observability"
- iconType="logoObservability"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="observability"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Observability"
+ paddingSize="none"
>
-
-
-
-
-
-
-
- Observability
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
-
-
- Observability
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Security
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-securitySolution"
- iconType="logoSecurity"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="securitySolution"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Security"
+ paddingSize="none"
>
-
-
-
-
-
-
-
- Security
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
-
-
- Security
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Management
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-management"
- iconType="managementApp"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="management"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Management"
+ paddingSize="none"
>
-
-
-
-
-
-
-
- Management
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
-
-
- Management
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+ monitoring
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- Dock navigation
-
- ,
- }
- }
- color="subdued"
- data-test-subj="collapsible-nav-lock"
- iconType="lockOpen"
- label="Dock navigation"
- onClick={[Function]}
- size="xs"
- >
-
-
+
-
-
-
+ data-euiicon-type="lockOpen"
+ />
Dock navigation
-
-
-
-
-
-
+ ,
+ }
+ }
+ color="subdued"
+ data-test-subj="collapsible-nav-lock"
+ iconType="lockOpen"
+ label="Dock navigation"
+ onClick={[Function]}
+ size="xs"
+ >
+
+
+
+
+
+
+ Dock navigation
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- close
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
`;
@@ -2770,42 +2706,57 @@ exports[`CollapsibleNav renders the default nav 3`] = `
isOpen={false}
onClose={[Function]}
>
-
-
-
-
-
-
-
-
+ data-euiicon-type="home"
+ />
+
+
+ Home
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+ Recently viewed
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-recentlyViewed"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="recentlyViewed"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Recently viewed"
+ paddingSize="none"
>
-
-
-
-
- Recently viewed
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
-
-
- Recently viewed
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
- No recently viewed items
-
-
-
-
-
-
+
+ No recently viewed items
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
-
-
-
- Undock navigation
-
- ,
- }
- }
- color="subdued"
- data-test-subj="collapsible-nav-lock"
- iconType="lock"
- label="Undock navigation"
- onClick={[Function]}
- size="xs"
- >
-
-
-
-
-
+ data-euiicon-type="lock"
+ />
Undock navigation
-
-
-
-
-
-
+ ,
+ }
+ }
+ color="subdued"
+ data-test-subj="collapsible-nav-lock"
+ iconType="lock"
+ label="Undock navigation"
+ onClick={[Function]}
+ size="xs"
+ >
+
+
+
+
+
+
+ Undock navigation
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- close
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
`;
diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap
index 6ad1e2d3a1cc6..5aee9ca1b7c08 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap
@@ -4947,42 +4947,57 @@ exports[`Header renders 1`] = `
isOpen={false}
onClose={[Function]}
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Manage cloud deployment
-
-
-
-
-
-
-
+ Manage cloud deployment
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+ data-euiicon-type="home"
+ />
+
+
+ Home
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+ Recently viewed
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-recentlyViewed"
+ id="mockId"
initialIsOpen={true}
- isCollapsible={true}
- key="recentlyViewed"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Recently viewed"
+ paddingSize="none"
>
-
-
-
-
- Recently viewed
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
-
-
- Recently viewed
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+ dashboard
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+ kibana
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Undock navigation
+
+ ,
+ }
+ }
+ color="subdued"
+ data-test-subj="collapsible-nav-lock"
+ iconType="lock"
+ label="Undock navigation"
onClick={[Function]}
- size="s"
+ size="xs"
>
+
+
+
- kibana
+ Undock navigation
@@ -5445,163 +5540,11 @@ exports[`Header renders 1`] = `
-
-
-
-
-
-
-
-
-
- Undock navigation
-
- ,
- }
- }
- color="subdued"
- data-test-subj="collapsible-nav-lock"
- iconType="lock"
- label="Undock navigation"
- onClick={[Function]}
- size="xs"
- >
-
-
-
-
-
-
- Undock navigation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- close
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx
index 7f338a859e7b4..460770744d53a 100644
--- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx
+++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx
@@ -16,10 +16,6 @@ import { httpServiceMock } from '../../../http/http_service.mock';
import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed';
import { CollapsibleNav } from './collapsible_nav';
-jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
- htmlIdGenerator: () => () => 'mockId',
-}));
-
const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES;
function mockLink({ title = 'discover', category }: Partial) {
diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx
index fdbdde8556eeb..a3a0197b4017e 100644
--- a/src/core/public/chrome/ui/header/header.test.tsx
+++ b/src/core/public/chrome/ui/header/header.test.tsx
@@ -99,7 +99,7 @@ describe('Header', () => {
act(() => isLocked$.next(true));
component.update();
- expect(component.find('nav[aria-label="Primary"]').exists()).toBeTruthy();
+ expect(component.find('[data-test-subj="collapsibleNav"]').exists()).toBeTruthy();
expect(component).toMatchSnapshot();
act(() =>
diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx
index 67cdd24aae848..246ca83ef5ade 100644
--- a/src/core/public/chrome/ui/header/header.tsx
+++ b/src/core/public/chrome/ui/header/header.tsx
@@ -87,6 +87,7 @@ export function Header({
const isVisible = useObservable(observables.isVisible$, false);
const isLocked = useObservable(observables.isLocked$, false);
const [isNavOpen, setIsNavOpen] = useState(false);
+ const [navId] = useState(htmlIdGenerator()());
const breadcrumbsAppendExtension = useObservable(breadcrumbsAppendExtension$);
if (!isVisible) {
@@ -99,7 +100,6 @@ export function Header({
}
const toggleCollapsibleNavRef = createRef void }>();
- const navId = htmlIdGenerator()();
const className = classnames('hide-for-sharing', 'headerGlobalNav');
const Breadcrumbs = (
diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts
index 1c4e78f0a5c2e..8ead0f50785bd 100644
--- a/src/core/public/core_system.test.ts
+++ b/src/core/public/core_system.test.ts
@@ -46,6 +46,7 @@ const defaultCoreSystemParams = {
csp: {
warnLegacyBrowsers: true,
},
+ version: 'version',
} as any,
};
@@ -91,12 +92,12 @@ describe('constructor', () => {
});
});
- it('passes browserSupportsCsp to ChromeService', () => {
+ it('passes browserSupportsCsp and coreContext to ChromeService', () => {
createCoreSystem();
-
expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1);
expect(ChromeServiceConstructor).toHaveBeenCalledWith({
- browserSupportsCsp: expect.any(Boolean),
+ browserSupportsCsp: true,
+ kibanaVersion: 'version',
});
});
diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts
index f0ea1e62fc33f..9a28bf45df927 100644
--- a/src/core/public/core_system.ts
+++ b/src/core/public/core_system.ts
@@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
import { CoreId } from '../server';
import { PackageInfo, EnvironmentMode } from '../server/types';
import { CoreSetup, CoreStart } from '.';
@@ -98,6 +97,7 @@ export class CoreSystem {
this.injectedMetadata = new InjectedMetadataService({
injectedMetadata,
});
+ this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env };
this.fatalErrors = new FatalErrorsService(rootDomElement, () => {
// Stop Core before rendering any fatal errors into the DOM
@@ -109,14 +109,16 @@ export class CoreSystem {
this.savedObjects = new SavedObjectsService();
this.uiSettings = new UiSettingsService();
this.overlay = new OverlayService();
- this.chrome = new ChromeService({ browserSupportsCsp });
+ this.chrome = new ChromeService({
+ browserSupportsCsp,
+ kibanaVersion: injectedMetadata.version,
+ });
this.docLinks = new DocLinksService();
this.rendering = new RenderingService();
this.application = new ApplicationService();
this.integrations = new IntegrationsService();
this.deprecations = new DeprecationsService();
- this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env };
this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins);
this.coreApp = new CoreApp(this.coreContext);
}
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 06277d9351922..502b22a6f8e89 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -22,6 +22,7 @@ export class DocLinksService {
const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/';
const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`;
const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`;
+ const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`;
const PLUGIN_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`;
return deepFreeze({
@@ -136,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`,
@@ -202,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`,
@@ -400,6 +403,19 @@ export class DocLinksService {
urlDecode: `${ELASTICSEARCH_DOCS}urldecode-processor.html`,
userAgent: `${ELASTICSEARCH_DOCS}user-agent-processor.html`,
},
+ fleet: {
+ guide: `${FLEET_DOCS}index.html`,
+ fleetServer: `${FLEET_DOCS}fleet-server.html`,
+ fleetServerAddFleetServer: `${FLEET_DOCS}fleet-server.html#add-fleet-server`,
+ settings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`,
+ settingsFleetServerHostSettings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`,
+ troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`,
+ elasticAgent: `${FLEET_DOCS}elastic-agent-installation-configuration.html`,
+ datastreams: `${FLEET_DOCS}data-streams.html`,
+ datastreamsNamingScheme: `${FLEET_DOCS}data-streams.html#data-streams-naming-scheme`,
+ upgradeElasticAgent: `${FLEET_DOCS}upgrade-elastic-agent.html`,
+ upgradeElasticAgent712lower: `${FLEET_DOCS}upgrade-elastic-agent.html#upgrade-7.12-lower`,
+ },
},
});
}
@@ -508,6 +524,7 @@ export interface DocLinksStart {
};
readonly search: {
readonly sessions: string;
+ readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
@@ -518,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;
@@ -587,5 +605,18 @@ export interface DocLinksStart {
readonly plugins: Record;
readonly snapshotRestore: Record;
readonly ingest: Record;
+ 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;
+ }>;
};
}
diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
index f5a1c51ccbe15..fbd09f3096854 100644
--- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
+++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
@@ -26,7 +26,7 @@ Array [
]
`;
-exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `""`;
+exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `""`;
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = `
Array [
@@ -59,4 +59,4 @@ Array [
]
`;
-exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `""`;
+exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `""`;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index d3426b50f7614..ca95b253f9cdb 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -585,6 +585,7 @@ export interface DocLinksStart {
};
readonly search: {
readonly sessions: string;
+ readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
@@ -595,6 +596,7 @@ export interface DocLinksStart {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
@@ -664,6 +666,19 @@ export interface DocLinksStart {
readonly plugins: Record;
readonly snapshotRestore: Record;
readonly ingest: Record;
+ 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;
+ }>;
};
}
@@ -1617,6 +1632,6 @@ export interface UserProvidedValues {
// Warnings were encountered during analysis:
//
-// src/core/public/core_system.ts:166:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
+// src/core/public/core_system.ts:168:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
```
diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss
index 4bd6afe90d342..92ba28ff70887 100644
--- a/src/core/public/rendering/_base.scss
+++ b/src/core/public/rendering/_base.scss
@@ -38,6 +38,7 @@
@mixin kbnAffordForHeader($headerHeight) {
@include euiHeaderAffordForFixed($headerHeight);
+ #securitySolutionStickyKQL,
#app-fixed-viewport {
top: $headerHeight;
}
diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss
index 3386fa73f328a..de138cdf402e6 100644
--- a/src/core/public/styles/_base.scss
+++ b/src/core/public/styles/_base.scss
@@ -26,7 +26,7 @@
}
.euiBody--collapsibleNavIsDocked .euiBottomBar {
- margin-left: $euiCollapsibleNavWidth;
+ margin-left: 320px; // Hard-coded for now -- @cchaos
}
// Temporary fix for EuiPageHeader with a bottom border but no tabs or padding
diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts
index dc24f889cd8dd..afe1b45175f86 100644
--- a/src/core/server/core_usage_data/core_usage_data_service.ts
+++ b/src/core/server/core_usage_data/core_usage_data_service.ts
@@ -126,14 +126,12 @@ export class CoreUsageDataService implements CoreService any) => fn.bind(null, {});
let certificate: string;
let key: string;
-beforeAll(() => {
- certificate = readFileSync(KBN_CERT_PATH, 'utf8');
- key = readFileSync(KBN_KEY_PATH, 'utf8');
+beforeAll(async () => {
+ certificate = await readFile(KBN_CERT_PATH, 'utf8');
+ key = await readFile(KBN_KEY_PATH, 'utf8');
});
beforeEach(() => {
@@ -1409,6 +1410,19 @@ describe('setup contract', () => {
});
describe('#registerStaticDir', () => {
+ const assetFolder = join(__dirname, 'integration_tests', 'fixtures', 'static');
+ let tempDir: string;
+
+ beforeAll(async () => {
+ tempDir = await mkdtemp('cache-test');
+ });
+
+ afterAll(async () => {
+ if (tempDir) {
+ await rmdir(tempDir, { recursive: true });
+ }
+ });
+
test('does not throw if called after stop', async () => {
const { registerStaticDir } = await server.setup(config);
await server.stop();
@@ -1416,6 +1430,111 @@ describe('setup contract', () => {
registerStaticDir('/path1/{path*}', '/path/to/resource');
}).not.toThrow();
});
+
+ test('returns correct headers for static assets', async () => {
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+
+ registerStaticDir('/static/{path*}', assetFolder);
+
+ await server.start();
+ const response = await supertest(innerServer.listener)
+ .get('/static/some_json.json')
+ .expect(200);
+
+ expect(response.get('cache-control')).toEqual('must-revalidate');
+ expect(response.get('etag')).not.toBeUndefined();
+ });
+
+ test('returns compressed version if present', async () => {
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+
+ registerStaticDir('/static/{path*}', assetFolder);
+
+ await server.start();
+ const response = await supertest(innerServer.listener)
+ .get('/static/compression_available.json')
+ .set('accept-encoding', 'gzip')
+ .expect(200);
+
+ expect(response.get('cache-control')).toEqual('must-revalidate');
+ expect(response.get('etag')).not.toBeUndefined();
+ expect(response.get('content-encoding')).toEqual('gzip');
+ });
+
+ test('returns uncompressed version if compressed asset is not available', async () => {
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+
+ registerStaticDir('/static/{path*}', assetFolder);
+
+ await server.start();
+ const response = await supertest(innerServer.listener)
+ .get('/static/some_json.json')
+ .set('accept-encoding', 'gzip')
+ .expect(200);
+
+ expect(response.get('cache-control')).toEqual('must-revalidate');
+ expect(response.get('etag')).not.toBeUndefined();
+ expect(response.get('content-encoding')).toBeUndefined();
+ });
+
+ test('returns a 304 if etag value matches', async () => {
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+
+ registerStaticDir('/static/{path*}', assetFolder);
+
+ await server.start();
+ const response = await supertest(innerServer.listener)
+ .get('/static/some_json.json')
+ .expect(200);
+
+ const etag = response.get('etag');
+ expect(etag).not.toBeUndefined();
+
+ await supertest(innerServer.listener)
+ .get('/static/some_json.json')
+ .set('If-None-Match', etag)
+ .expect(304);
+ });
+
+ test('serves content if etag values does not match', async () => {
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+
+ registerStaticDir('/static/{path*}', assetFolder);
+
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/static/some_json.json')
+ .set('If-None-Match', `"definitely not a valid etag"`)
+ .expect(200);
+ });
+
+ test('dynamically updates depending on the content of the file', async () => {
+ const tempFile = join(tempDir, 'some_file.json');
+
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+ registerStaticDir('/static/{path*}', tempDir);
+
+ await server.start();
+
+ await supertest(innerServer.listener).get('/static/some_file.json').expect(404);
+
+ await writeFile(tempFile, `{ "over": 9000 }`);
+
+ let response = await supertest(innerServer.listener)
+ .get('/static/some_file.json')
+ .expect(200);
+
+ const etag1 = response.get('etag');
+
+ await writeFile(tempFile, `{ "over": 42 }`);
+
+ response = await supertest(innerServer.listener).get('/static/some_file.json').expect(200);
+
+ const etag2 = response.get('etag');
+
+ expect(etag1).not.toEqual(etag2);
+ });
});
describe('#registerOnPreRouting', () => {
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index 8b4c3b9416152..d43d86d587d06 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -465,7 +465,13 @@ export class HttpServer {
lookupCompressed: true,
},
},
- options: { auth: false },
+ options: {
+ auth: false,
+ cache: {
+ privacy: 'public',
+ otherwise: 'must-revalidate',
+ },
+ },
});
}
diff --git a/src/core/server/http/integration_tests/fixtures/static/compression_available.json b/src/core/server/http/integration_tests/fixtures/static/compression_available.json
new file mode 100644
index 0000000000000..1f878fb465cff
--- /dev/null
+++ b/src/core/server/http/integration_tests/fixtures/static/compression_available.json
@@ -0,0 +1,3 @@
+{
+ "hello": "dolly"
+}
diff --git a/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz b/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz
new file mode 100644
index 0000000000000..e77819d2e8e59
Binary files /dev/null and b/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz differ
diff --git a/src/core/server/http/integration_tests/fixtures/static/some_json.json b/src/core/server/http/integration_tests/fixtures/static/some_json.json
new file mode 100644
index 0000000000000..c8c4105eb57cd
--- /dev/null
+++ b/src/core/server/http/integration_tests/fixtures/static/some_json.json
@@ -0,0 +1,3 @@
+{
+ "foo": "bar"
+}
diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts
index 7571184363d2e..dfc47098724cc 100644
--- a/src/core/server/http/integration_tests/request.test.ts
+++ b/src/core/server/http/integration_tests/request.test.ts
@@ -163,24 +163,24 @@ describe('KibanaRequest', () => {
describe('events', () => {
describe('aborted$', () => {
- it('emits once and completes when request aborted', async (done) => {
+ it('emits once and completes when request aborted', async () => {
expect.assertions(1);
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
const nextSpy = jest.fn();
- router.get({ path: '/', validate: false }, async (context, request, res) => {
- request.events.aborted$.subscribe({
- next: nextSpy,
- complete: () => {
- expect(nextSpy).toHaveBeenCalledTimes(1);
- done();
- },
- });
- // prevents the server to respond
- await delay(30000);
- return res.ok({ body: 'ok' });
+ const done = new Promise((resolve) => {
+ router.get({ path: '/', validate: false }, async (context, request, res) => {
+ request.events.aborted$.subscribe({
+ next: nextSpy,
+ complete: resolve,
+ });
+
+ // prevents the server to respond
+ await delay(30000);
+ return res.ok({ body: 'ok' });
+ });
});
await server.start();
@@ -191,6 +191,8 @@ describe('KibanaRequest', () => {
.end();
setTimeout(() => incomingRequest.abort(), 50);
+ await done;
+ expect(nextSpy).toHaveBeenCalledTimes(1);
});
it('completes & does not emit when request handled', async () => {
@@ -299,25 +301,24 @@ describe('KibanaRequest', () => {
expect(completeSpy).toHaveBeenCalledTimes(1);
});
- it('emits once and completes when response is aborted', async (done) => {
+ it('emits once and completes when response is aborted', async () => {
expect.assertions(2);
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
const nextSpy = jest.fn();
- router.get({ path: '/', validate: false }, async (context, req, res) => {
- req.events.completed$.subscribe({
- next: nextSpy,
- complete: () => {
- expect(nextSpy).toHaveBeenCalledTimes(1);
- done();
- },
- });
+ const done = new Promise((resolve) => {
+ router.get({ path: '/', validate: false }, async (context, req, res) => {
+ req.events.completed$.subscribe({
+ next: nextSpy,
+ complete: resolve,
+ });
- expect(nextSpy).not.toHaveBeenCalled();
- await delay(30000);
- return res.ok({ body: 'ok' });
+ expect(nextSpy).not.toHaveBeenCalled();
+ await delay(30000);
+ return res.ok({ body: 'ok' });
+ });
});
await server.start();
@@ -327,6 +328,8 @@ describe('KibanaRequest', () => {
// end required to send request
.end();
setTimeout(() => incomingRequest.abort(), 50);
+ await done;
+ expect(nextSpy).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts
index 14dba1db9b624..0ec6fe89de1f1 100644
--- a/src/core/server/saved_objects/migrations/core/index_migrator.ts
+++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts
@@ -187,12 +187,8 @@ async function migrateSourceToDest(context: Context) {
await Index.write(
client,
dest.indexName,
- await migrateRawDocs(
- serializer,
- documentMigrator.migrateAndConvert,
- // @ts-expect-error @elastic/elasticsearch `Hit._id` may be a string | number in ES, but we always expect strings in the SO index.
- docs
- )
+ // @ts-expect-error @elastic/elasticsearch _source is optional
+ await migrateRawDocs(serializer, documentMigrator.migrateAndConvert, docs)
);
}
}
diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts
index f4e0dd8fffcab..4c9e37d17f2e7 100644
--- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts
@@ -95,6 +95,12 @@ describe('migration v2', () => {
},
],
},
+ // reporting loads headless browser, that prevents nodejs process from exiting.
+ xpack: {
+ reporting: {
+ enabled: false,
+ },
+ },
},
{
oss,
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 22c40a547f419..4456784fdbc0b 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -525,15 +525,22 @@ describe('SavedObjectsRepository', () => {
const ns2 = 'bar-namespace';
const ns3 = 'baz-namespace';
const objects = [
- { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2] },
- { ...obj2, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns3] },
+ { ...obj1, type: 'dashboard', initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] },
];
await bulkCreateSuccess(objects, { namespace, overwrite: true });
const body = [
- expect.any(Object),
+ { index: expect.objectContaining({ _id: `${ns2}:dashboard:${obj1.id}` }) },
+ expect.objectContaining({ namespace: ns2 }),
+ {
+ index: expect.objectContaining({
+ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj1.id}`,
+ }),
+ },
expect.objectContaining({ namespaces: [ns2] }),
- expect.any(Object),
- expect.objectContaining({ namespaces: [ns3] }),
+ { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj1.id}` }) },
+ expect.objectContaining({ namespaces: [ns2, ns3] }),
];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
@@ -649,24 +656,19 @@ describe('SavedObjectsRepository', () => {
).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"'));
});
- it(`returns error when initialNamespaces is used with a non-shareable object`, async () => {
- const test = async (objType) => {
- const obj = { ...obj3, type: objType, initialNamespaces: [] };
- await bulkCreateError(
+ it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => {
+ const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
obj,
- undefined,
- expectErrorResult(
- obj,
- createBadRequestError('"initialNamespaces" can only be used on multi-namespace types')
- )
- );
- };
- await test('dashboard');
- await test(NAMESPACE_AGNOSTIC_TYPE);
- await test(MULTI_NAMESPACE_ISOLATED_TYPE);
+ createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types')
+ )
+ );
});
- it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => {
+ it(`returns error when initialNamespaces is empty`, async () => {
const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] };
await bulkCreateError(
obj,
@@ -678,6 +680,26 @@ describe('SavedObjectsRepository', () => {
);
});
+ it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType, initialNamespaces) => {
+ const obj = { ...obj3, type: objType, initialNamespaces };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
+ obj,
+ createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
it(`returns error when type is invalid`, async () => {
const obj = { ...obj3, type: 'unknownType' };
await bulkCreateError(obj, undefined, expectErrorInvalidType(obj));
@@ -1865,12 +1887,46 @@ describe('SavedObjectsRepository', () => {
});
it(`adds initialNamespaces instead of namespace`, async () => {
- const options = { id, namespace, initialNamespaces: ['bar-namespace', 'baz-namespace'] };
- await createSuccess(MULTI_NAMESPACE_TYPE, attributes, options);
- expect(client.create).toHaveBeenCalledWith(
+ const ns2 = 'bar-namespace';
+ const ns3 = 'baz-namespace';
+ await savedObjectsRepository.create('dashboard', attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2],
+ });
+ await savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2],
+ });
+ await savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2, ns3],
+ });
+
+ expect(client.create).toHaveBeenCalledTimes(3);
+ expect(client.create).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ id: `${ns2}:dashboard:${id}`,
+ body: expect.objectContaining({ namespace: ns2 }),
+ }),
+ expect.anything()
+ );
+ expect(client.create).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`,
+ body: expect.objectContaining({ namespaces: [ns2] }),
+ }),
+ expect.anything()
+ );
+ expect(client.create).toHaveBeenNthCalledWith(
+ 3,
expect.objectContaining({
id: `${MULTI_NAMESPACE_TYPE}:${id}`,
- body: expect.objectContaining({ namespaces: options.initialNamespaces }),
+ body: expect.objectContaining({ namespaces: [ns2, ns3] }),
}),
expect.anything()
);
@@ -1892,29 +1948,40 @@ describe('SavedObjectsRepository', () => {
});
describe('errors', () => {
- it(`throws when options.initialNamespaces is used with a non-shareable object`, async () => {
- const test = async (objType) => {
- await expect(
- savedObjectsRepository.create(objType, attributes, { initialNamespaces: [namespace] })
- ).rejects.toThrowError(
- createBadRequestError(
- '"options.initialNamespaces" can only be used on multi-namespace types'
- )
- );
- };
- await test('dashboard');
- await test(MULTI_NAMESPACE_ISOLATED_TYPE);
- await test(NAMESPACE_AGNOSTIC_TYPE);
+ it(`throws when options.initialNamespaces is used with a space-agnostic object`, async () => {
+ await expect(
+ savedObjectsRepository.create(NAMESPACE_AGNOSTIC_TYPE, attributes, {
+ initialNamespaces: [namespace],
+ })
+ ).rejects.toThrowError(
+ createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types')
+ );
});
- it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => {
+ it(`throws when options.initialNamespaces is empty`, async () => {
await expect(
savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { initialNamespaces: [] })
).rejects.toThrowError(
- createBadRequestError('"options.initialNamespaces" must be a non-empty array of strings')
+ createBadRequestError('"initialNamespaces" must be a non-empty array of strings')
);
});
+ it(`throws when options.initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType, initialNamespaces) => {
+ await expect(
+ savedObjectsRepository.create(objType, attributes, { initialNamespaces })
+ ).rejects.toThrowError(
+ createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
it(`throws when options.namespace is '*'`, async () => {
await expect(
savedObjectsRepository.create(type, attributes, { namespace: ALL_NAMESPACES_STRING })
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index 6b51bd57248a1..c9fa50da55df1 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -283,28 +283,18 @@ export class SavedObjectsRepository {
} = options;
const namespace = normalizeNamespace(options.namespace);
- if (initialNamespaces) {
- if (!this._registry.isShareable(type)) {
- throw SavedObjectsErrorHelpers.createBadRequestError(
- '"options.initialNamespaces" can only be used on multi-namespace types'
- );
- } else if (!initialNamespaces.length) {
- throw SavedObjectsErrorHelpers.createBadRequestError(
- '"options.initialNamespaces" must be a non-empty array of strings'
- );
- }
- }
+ this.validateInitialNamespaces(type, initialNamespaces);
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
}
const time = this._getCurrentTime();
- let savedObjectNamespace;
+ let savedObjectNamespace: string | undefined;
let savedObjectNamespaces: string[] | undefined;
- if (this._registry.isSingleNamespace(type) && namespace) {
- savedObjectNamespace = namespace;
+ if (this._registry.isSingleNamespace(type)) {
+ savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace;
} else if (this._registry.isMultiNamespace(type)) {
if (id && overwrite) {
// we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces
@@ -369,32 +359,29 @@ export class SavedObjectsRepository {
let bulkGetRequestIndexCounter = 0;
const expectedResults: Either[] = objects.map((object) => {
+ const { type, id, initialNamespaces } = object;
let error: DecoratedError | undefined;
- if (!this._allowedTypes.includes(object.type)) {
- error = SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type);
- } else if (object.initialNamespaces) {
- if (!this._registry.isShareable(object.type)) {
- error = SavedObjectsErrorHelpers.createBadRequestError(
- '"initialNamespaces" can only be used on multi-namespace types'
- );
- } else if (!object.initialNamespaces.length) {
- error = SavedObjectsErrorHelpers.createBadRequestError(
- '"initialNamespaces" must be a non-empty array of strings'
- );
+ if (!this._allowedTypes.includes(type)) {
+ error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
+ } else {
+ try {
+ this.validateInitialNamespaces(type, initialNamespaces);
+ } catch (e) {
+ error = e;
}
}
if (error) {
return {
tag: 'Left' as 'Left',
- error: { id: object.id, type: object.type, error: errorContent(error) },
+ error: { id, type, error: errorContent(error) },
};
}
- const method = object.id && overwrite ? 'index' : 'create';
- const requiresNamespacesCheck = object.id && this._registry.isMultiNamespace(object.type);
+ const method = id && overwrite ? 'index' : 'create';
+ const requiresNamespacesCheck = id && this._registry.isMultiNamespace(type);
- if (object.id == null) {
+ if (id == null) {
object.id = SavedObjectsUtils.generateId();
}
@@ -434,8 +421,8 @@ export class SavedObjectsRepository {
return expectedBulkGetResult;
}
- let savedObjectNamespace;
- let savedObjectNamespaces;
+ let savedObjectNamespace: string | undefined;
+ let savedObjectNamespaces: string[] | undefined;
let versionProperties;
const {
esRequestIndex,
@@ -469,7 +456,7 @@ export class SavedObjectsRepository {
versionProperties = getExpectedVersionProperties(version, actualResult);
} else {
if (this._registry.isSingleNamespace(object.type)) {
- savedObjectNamespace = namespace;
+ savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace;
} else if (this._registry.isMultiNamespace(object.type)) {
savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace);
}
@@ -897,10 +884,10 @@ export class SavedObjectsRepository {
total: body.hits.total,
saved_objects: body.hits.hits.map(
(hit: estypes.SearchHit): SavedObjectsFindResult => ({
- // @ts-expect-error @elastic/elasticsearch declared Id as string | number
+ // @ts-expect-error @elastic/elasticsearch _source is optional
...this._rawToSavedObject(hit),
score: hit._score!,
- // @ts-expect-error @elastic/elasticsearch declared sort as string | number
+ // @ts-expect-error @elastic/elasticsearch _source is optional
sort: hit.sort,
})
),
@@ -2080,6 +2067,29 @@ export class SavedObjectsRepository {
const object = await this.get(type, id, options);
return { saved_object: object, outcome: 'exactMatch' };
}
+
+ private validateInitialNamespaces(type: string, initialNamespaces: string[] | undefined) {
+ if (!initialNamespaces) {
+ return;
+ }
+
+ if (this._registry.isNamespaceAgnostic(type)) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" cannot be used on space-agnostic types'
+ );
+ } else if (!initialNamespaces.length) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" must be a non-empty array of strings'
+ );
+ } else if (
+ !this._registry.isShareable(type) &&
+ (initialNamespaces.length > 1 || initialNamespaces.includes(ALL_NAMESPACES_STRING))
+ ) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ );
+ }
+ }
}
/**
diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts
index af682cfb81296..1423050145695 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.ts
@@ -63,7 +63,11 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
- * Note: this can only be used for multi-namespace object types.
+ * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
+ * including the "All spaces" identifier (`'*'`).
+ * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
+ * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+ * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
@@ -96,7 +100,11 @@ export interface SavedObjectsBulkCreateObject {
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
- * Note: this can only be used for multi-namespace object types.
+ * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
+ * including the "All spaces" identifier (`'*'`).
+ * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
+ * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+ * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 9e7721fde90e7..fcecf39f7e53a 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2901,7 +2901,7 @@ export class SavedObjectsRepository {
resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise;
-}
+ }
// @public
export interface SavedObjectsRepositoryFactory {
diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts
index 534d7df9d9466..e1986c5bf1d92 100644
--- a/src/core/server/server.test.ts
+++ b/src/core/server/server.test.ts
@@ -114,6 +114,7 @@ test('runs services on "start"', async () => {
expect(mockSavedObjectsService.start).not.toHaveBeenCalled();
expect(mockUiSettingsService.start).not.toHaveBeenCalled();
expect(mockMetricsService.start).not.toHaveBeenCalled();
+ expect(mockStatusService.start).not.toHaveBeenCalled();
await server.start();
@@ -121,6 +122,7 @@ test('runs services on "start"', async () => {
expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1);
expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1);
expect(mockMetricsService.start).toHaveBeenCalledTimes(1);
+ expect(mockStatusService.start).toHaveBeenCalledTimes(1);
});
test('does not fail on "setup" if there are unused paths detected', async () => {
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index adf794c390338..3f553dd90678e 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -248,6 +248,7 @@ export class Server {
savedObjects: savedObjectsStart,
exposedConfigsToUsage: this.plugins.getExposedPluginConfigsToUsage(),
});
+ this.status.start();
this.coreStart = {
capabilities: capabilitiesStart,
@@ -261,7 +262,6 @@ export class Server {
await this.plugins.start(this.coreStart);
- this.status.start();
await this.http.start();
startTransaction?.end();
diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts
index b0d9e47876940..9dc1ddcddca3e 100644
--- a/src/core/server/status/plugins_status.test.ts
+++ b/src/core/server/status/plugins_status.test.ts
@@ -8,7 +8,7 @@
import { PluginName } from '../plugins';
import { PluginsStatusService } from './plugins_status';
-import { of, Observable, BehaviorSubject } from 'rxjs';
+import { of, Observable, BehaviorSubject, ReplaySubject } from 'rxjs';
import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types';
import { first } from 'rxjs/operators';
import { ServiceStatusLevelSnapshotSerializer } from './test_utils';
@@ -34,6 +34,28 @@ describe('PluginStatusService', () => {
['c', ['a', 'b']],
]);
+ describe('set', () => {
+ it('throws an exception if called after registrations are blocked', () => {
+ const service = new PluginsStatusService({
+ core$: coreAllAvailable$,
+ pluginDependencies,
+ });
+
+ service.blockNewRegistrations();
+ expect(() => {
+ service.set(
+ 'a',
+ of({
+ level: ServiceStatusLevels.available,
+ summary: 'fail!',
+ })
+ );
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"Custom statuses cannot be registered after setup, plugin [a] attempted"`
+ );
+ });
+ });
+
describe('getDerivedStatus$', () => {
it(`defaults to core's most severe status`, async () => {
const serviceAvailable = new PluginsStatusService({
@@ -231,6 +253,75 @@ describe('PluginStatusService', () => {
{ a: { level: ServiceStatusLevels.available, summary: 'a available' } },
]);
});
+
+ it('updates when a plugin status observable emits', async () => {
+ const service = new PluginsStatusService({
+ core$: coreAllAvailable$,
+ pluginDependencies: new Map([['a', []]]),
+ });
+ const statusUpdates: Array> = [];
+ const subscription = service
+ .getAll$()
+ .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses));
+
+ const aStatus$ = new BehaviorSubject({
+ level: ServiceStatusLevels.degraded,
+ summary: 'a degraded',
+ });
+ service.set('a', aStatus$);
+ aStatus$.next({ level: ServiceStatusLevels.unavailable, summary: 'a unavailable' });
+ aStatus$.next({ level: ServiceStatusLevels.available, summary: 'a available' });
+ subscription.unsubscribe();
+
+ expect(statusUpdates).toEqual([
+ { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } },
+ { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } },
+ { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } },
+ { a: { level: ServiceStatusLevels.available, summary: 'a available' } },
+ ]);
+ });
+
+ it('emits an unavailable status if first emission times out, then continues future emissions', async () => {
+ jest.useFakeTimers();
+ const service = new PluginsStatusService({
+ core$: coreAllAvailable$,
+ pluginDependencies: new Map([
+ ['a', []],
+ ['b', ['a']],
+ ]),
+ });
+
+ const pluginA$ = new ReplaySubject(1);
+ service.set('a', pluginA$);
+ const firstEmission = service.getAll$().pipe(first()).toPromise();
+ jest.runAllTimers();
+
+ expect(await firstEmission).toEqual({
+ a: { level: ServiceStatusLevels.unavailable, summary: 'Status check timed out after 30s' },
+ b: {
+ level: ServiceStatusLevels.unavailable,
+ summary: '[a]: Status check timed out after 30s',
+ detail: 'See the status page for more information',
+ meta: {
+ affectedServices: {
+ a: {
+ level: ServiceStatusLevels.unavailable,
+ summary: 'Status check timed out after 30s',
+ },
+ },
+ },
+ },
+ });
+
+ pluginA$.next({ level: ServiceStatusLevels.available, summary: 'a available' });
+ const secondEmission = service.getAll$().pipe(first()).toPromise();
+ jest.runAllTimers();
+ expect(await secondEmission).toEqual({
+ a: { level: ServiceStatusLevels.available, summary: 'a available' },
+ b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' },
+ });
+ jest.useRealTimers();
+ });
});
describe('getDependenciesStatus$', () => {
diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts
index 1aacbf3be56db..6a8ef1081e165 100644
--- a/src/core/server/status/plugins_status.ts
+++ b/src/core/server/status/plugins_status.ts
@@ -7,13 +7,22 @@
*/
import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs';
-import { map, distinctUntilChanged, switchMap, debounceTime } from 'rxjs/operators';
+import {
+ map,
+ distinctUntilChanged,
+ switchMap,
+ debounceTime,
+ timeoutWith,
+ startWith,
+} from 'rxjs/operators';
import { isDeepStrictEqual } from 'util';
import { PluginName } from '../plugins';
-import { ServiceStatus, CoreStatus } from './types';
+import { ServiceStatus, CoreStatus, ServiceStatusLevels } from './types';
import { getSummaryStatus } from './get_summary_status';
+const STATUS_TIMEOUT_MS = 30 * 1000; // 30 seconds
+
interface Deps {
core$: Observable;
pluginDependencies: ReadonlyMap;
@@ -23,6 +32,7 @@ export class PluginsStatusService {
private readonly pluginStatuses = new Map>();
private readonly update$ = new BehaviorSubject(true);
private readonly defaultInheritedStatus$: Observable;
+ private newRegistrationsAllowed = true;
constructor(private readonly deps: Deps) {
this.defaultInheritedStatus$ = this.deps.core$.pipe(
@@ -35,10 +45,19 @@ export class PluginsStatusService {
}
public set(plugin: PluginName, status$: Observable) {
+ if (!this.newRegistrationsAllowed) {
+ throw new Error(
+ `Custom statuses cannot be registered after setup, plugin [${plugin}] attempted`
+ );
+ }
this.pluginStatuses.set(plugin, status$);
this.update$.next(true); // trigger all existing Observables to update from the new source Observable
}
+ public blockNewRegistrations() {
+ this.newRegistrationsAllowed = false;
+ }
+
public getAll$(): Observable> {
return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]);
}
@@ -86,13 +105,22 @@ export class PluginsStatusService {
return this.update$.pipe(
switchMap(() => {
const pluginStatuses = plugins
- .map(
- (depName) =>
- [depName, this.pluginStatuses.get(depName) ?? this.getDerivedStatus$(depName)] as [
- PluginName,
- Observable
- ]
- )
+ .map((depName) => {
+ const pluginStatus = this.pluginStatuses.get(depName)
+ ? this.pluginStatuses.get(depName)!.pipe(
+ timeoutWith(
+ STATUS_TIMEOUT_MS,
+ this.pluginStatuses.get(depName)!.pipe(
+ startWith({
+ level: ServiceStatusLevels.unavailable,
+ summary: `Status check timed out after ${STATUS_TIMEOUT_MS / 1000}s`,
+ })
+ )
+ )
+ )
+ : this.getDerivedStatus$(depName);
+ return [depName, pluginStatus] as [PluginName, Observable];
+ })
.map(([pName, status$]) =>
status$.pipe(map((status) => [pName, status] as [PluginName, ServiceStatus]))
);
diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts
index b8c19508a5d61..d4dc8ed3d4d72 100644
--- a/src/core/server/status/status_service.ts
+++ b/src/core/server/status/status_service.ts
@@ -135,9 +135,11 @@ export class StatusService implements CoreService {
}
public start() {
- if (!this.overall$) {
- throw new Error('cannot call `start` before `setup`');
+ if (!this.pluginsStatus || !this.overall$) {
+ throw new Error(`StatusService#setup must be called before #start`);
}
+ this.pluginsStatus.blockNewRegistrations();
+
getOverallStatusChanges(this.overall$, this.stop$).subscribe((message) => {
this.logger.info(message);
});
diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts
index 411b942c8eb33..bfca4c74d9365 100644
--- a/src/core/server/status/types.ts
+++ b/src/core/server/status/types.ts
@@ -196,6 +196,9 @@ export interface StatusServiceSetup {
* Completely overrides the default inherited status.
*
* @remarks
+ * The first emission from this Observable should occur within 30s, else this plugin's status will fallback to
+ * `unavailable` until the first emission.
+ *
* See the {@link StatusServiceSetup.derivedStatus$} API for leveraging the default status
* calculation that is provided by Core.
*/
diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts
index 6c7cdfa43cf57..61e55284a20b8 100644
--- a/src/core/server/ui_settings/integration_tests/index.test.ts
+++ b/src/core/server/ui_settings/integration_tests/index.test.ts
@@ -17,7 +17,7 @@ const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.
const savedObjectIndex = `.kibana_${kibanaVersion}_001`;
describe('uiSettings/routes', function () {
- jest.setTimeout(10000);
+ jest.setTimeout(120_000);
beforeAll(startServers);
/* eslint-disable jest/valid-describe */
diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts
index b18d9926649aa..96ba08a0728ab 100644
--- a/src/core/server/ui_settings/integration_tests/lib/servers.ts
+++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts
@@ -75,8 +75,10 @@ export function getServices() {
export async function stopServers() {
services = null!;
- if (servers) {
+ if (esServer) {
await esServer.stop();
+ }
+ if (kbn) {
await kbn.stop();
}
}
diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts
index ba22ecb3b6376..2995ffd08e5c0 100644
--- a/src/core/test_helpers/kbn_server.ts
+++ b/src/core/test_helpers/kbn_server.ts
@@ -7,15 +7,7 @@
*/
import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils';
-import {
- createTestEsCluster,
- DEFAULT_SUPERUSER_PASS,
- esTestConfig,
- kbnTestConfig,
- kibanaServerTestUser,
- kibanaTestUser,
- setupUsers,
-} from '@kbn/test';
+import { createTestEsCluster, esTestConfig, kibanaServerTestUser, kibanaTestUser } from '@kbn/test';
import { defaultsDeep } from 'lodash';
import { resolve } from 'path';
import { BehaviorSubject } from 'rxjs';
@@ -208,7 +200,6 @@ export function createTestServers({
defaultsDeep({}, settings.es ?? {}, {
log,
license,
- password: license === 'trial' ? DEFAULT_SUPERUSER_PASS : undefined,
})
);
@@ -224,19 +215,7 @@ export function createTestServers({
await es.start();
if (['gold', 'trial'].includes(license)) {
- await setupUsers({
- log,
- esPort: esTestConfig.getUrlParts().port,
- updates: [
- ...usersToBeAdded,
- // user elastic
- esTestConfig.getUrlParts() as { username: string; password: string },
- // user kibana
- kbnTestConfig.getUrlParts() as { username: string; password: string },
- ],
- });
-
- // Override provided configs, we know what the elastic user is now
+ // Override provided configs
kbnSettings.elasticsearch = {
hosts: [esTestConfig.getUrl()],
username: kibanaServerTestUser.username,
diff --git a/src/core/types/elasticsearch/search.ts b/src/core/types/elasticsearch/search.ts
index 36a684fb097a5..0960fb189a341 100644
--- a/src/core/types/elasticsearch/search.ts
+++ b/src/core/types/elasticsearch/search.ts
@@ -417,7 +417,9 @@ export type AggregateOf<
{
key: string;
from?: number;
+ from_as_string?: string;
to?: number;
+ to_as_string?: string;
doc_count: number;
},
TAggregationContainer extends { range: { ranges: Array } }
diff --git a/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat
new file mode 100755
index 0000000000000..9221af3142e61
--- /dev/null
+++ b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat
@@ -0,0 +1,35 @@
+@echo off
+
+SETLOCAL ENABLEDELAYEDEXPANSION
+
+set SCRIPT_DIR=%~dp0
+for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI
+
+set NODE=%DIR%\node\node.exe
+
+If Not Exist "%NODE%" (
+ Echo unable to find usable node.js executable.
+ Exit /B 1
+)
+
+set CONFIG_DIR=%KBN_PATH_CONF%
+If [%KBN_PATH_CONF%] == [] (
+ set "CONFIG_DIR=%DIR%\config"
+)
+
+IF EXIST "%CONFIG_DIR%\node.options" (
+ for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do (
+ If [!NODE_OPTIONS!] == [] (
+ set "NODE_OPTIONS=%%i"
+ ) Else (
+ set "NODE_OPTIONS=!NODE_OPTIONS! %%i"
+ )
+ )
+)
+
+TITLE Kibana Encryption Keys
+"%NODE%" "%DIR%\src\cli_encryption_keys\dist" %*
+
+:finally
+
+ENDLOCAL
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
index a9b2dd6aefdda..d109a824ca81d 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
@@ -69,7 +69,6 @@ kibana_vars=(
logging.appenders
logging.appenders.console
logging.appenders.file
- logging.appenders.rolling-file
logging.dest
logging.json
logging.loggers
@@ -204,8 +203,8 @@ kibana_vars=(
xpack.actions.proxyUrl
xpack.actions.rejectUnauthorized
xpack.actions.responseTimeout
- xpack.actions.tls.proxyVerificationMode
- xpack.actions.tls.verificationMode
+ xpack.actions.ssl.proxyVerificationMode
+ xpack.actions.ssl.verificationMode
xpack.alerting.healthCheck.interval
xpack.alerting.invalidateApiKeysTask.interval
xpack.alerting.invalidateApiKeysTask.removalDelay
diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts
index ebf56166a8922..b3b7bf5e8eed7 100644
--- a/src/dev/license_checker/config.ts
+++ b/src/dev/license_checker/config.ts
@@ -10,6 +10,7 @@
// used as dependencies or dev dependencies
export const LICENSE_ALLOWED = [
'Elastic-License',
+ 'Elastic License 2.0',
'SSPL-1.0 OR Elastic License 2.0',
'0BSD',
'(BSD-2-Clause OR MIT OR Apache-2.0)',
@@ -72,6 +73,7 @@ export const DEV_ONLY_LICENSE_ALLOWED = ['MPL-2.0'];
export const LICENSE_OVERRIDES = {
'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts
'@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint
+ '@elastic/ems-client@7.14.0': ['Elastic License 2.0'],
// TODO can be removed if the https://github.com/jindw/xmldom/issues/239 is released
'xmldom@0.1.27': ['MIT'],
diff --git a/src/dev/run_licenses_csv_report.js b/src/dev/run_licenses_csv_report.js
index 8a612c9e3d878..1923eddff33e9 100644
--- a/src/dev/run_licenses_csv_report.js
+++ b/src/dev/run_licenses_csv_report.js
@@ -71,7 +71,8 @@ run(
licenses: [
'Custom;https://www.redhat.com/licenses/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf',
],
- sourceURL: 'https://oss-dependencies.elastic.co/redhat/ubi/ubi-minimal-8-source.tar.gz',
+ sourceURL:
+ 'https://oss-dependencies.elastic.co/red-hat-universal-base-image-minimal/8/ubi-minimal-8-source.tar.gz',
}
);
diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts
index 050743114f657..2c54bb8dba179 100644
--- a/src/dev/typescript/projects.ts
+++ b/src/dev/typescript/projects.ts
@@ -22,6 +22,9 @@ export const PROJECTS = [
new Project(resolve(REPO_ROOT, 'x-pack/plugins/security_solution/cypress/tsconfig.json'), {
name: 'security_solution/cypress',
}),
+ new Project(resolve(REPO_ROOT, 'x-pack/plugins/osquery/cypress/tsconfig.json'), {
+ name: 'osquery/cypress',
+ }),
new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), {
name: 'apm/cypress',
disableTypeCheck: true,
@@ -55,6 +58,9 @@ export const PROJECTS = [
...glob
.sync('test/interpreter_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT })
.map((path) => new Project(resolve(REPO_ROOT, path))),
+ ...glob
+ .sync('test/server_integration/__fixtures__/plugins/*/tsconfig.json', { cwd: REPO_ROOT })
+ .map((path) => new Project(resolve(REPO_ROOT, path))),
];
export function filterProjectsByFlag(projectFlag?: string) {
diff --git a/src/plugins/charts/public/services/palettes/types.ts b/src/plugins/charts/public/services/palettes/types.ts
index 6f13f62178364..7a870504270d7 100644
--- a/src/plugins/charts/public/services/palettes/types.ts
+++ b/src/plugins/charts/public/services/palettes/types.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { Ast } from '@kbn/interpreter/common';
+import { ExpressionAstExpression } from '../../../../expressions/common/ast';
/**
* Information about a series in a chart used to determine its color.
@@ -78,7 +78,7 @@ export interface PaletteDefinition {
* This function should be used to pass the palette to the expression function applying color and other styles
* @param state The internal state of the palette
*/
- toExpression: (state?: T) => Ast;
+ toExpression: (state?: T) => ExpressionAstExpression;
/**
* Color a series according to the internal rules of the palette.
* @param series The current series along with its ancestors.
diff --git a/src/plugins/console/public/application/components/welcome_panel.tsx b/src/plugins/console/public/application/components/welcome_panel.tsx
index eb746e313d228..8514d41c04a51 100644
--- a/src/plugins/console/public/application/components/welcome_panel.tsx
+++ b/src/plugins/console/public/application/components/welcome_panel.tsx
@@ -27,7 +27,7 @@ interface Props {
export function WelcomePanel(props: Props) {
return (
-
+
diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
index 9f56740fdac22..afe339f3f43a2 100644
--- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
+++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
@@ -603,7 +603,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = `
}
>
-
-
+
@@ -950,7 +950,7 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = `
}
>
-
-
+
diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts
index 45724796c3518..d7b3c630d1a6e 100644
--- a/src/plugins/data/common/es_query/es_query/build_es_query.ts
+++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts
@@ -10,9 +10,9 @@ import { groupBy, has, isEqual } from 'lodash';
import { buildQueryFromKuery } from './from_kuery';
import { buildQueryFromFilters } from './from_filters';
import { buildQueryFromLucene } from './from_lucene';
-import { IIndexPattern } from '../../index_patterns';
import { Filter } from '../filters';
import { Query } from '../../query/types';
+import { IndexPatternBase } from './types';
export interface EsQueryConfig {
allowLeadingWildcards: boolean;
@@ -36,7 +36,7 @@ function removeMatchAll(filters: T[]) {
* config contains dateformat:tz
*/
export function buildEsQuery(
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
queries: Query | Query[],
filters: Filter | Filter[],
config: EsQueryConfig = {
diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts
index 478263d5ce601..b376436756092 100644
--- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts
+++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts
@@ -6,15 +6,16 @@
* Side Public License, v 1.
*/
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
import { Filter } from '../filters';
+import { IndexPatternBase } from './types';
/*
* TODO: We should base this on something better than `filter.meta.key`. We should probably modify
* this to check if `filter.meta.index` matches `indexPattern.id` instead, but that's a breaking
* change.
*/
-export function filterMatchesIndex(filter: Filter, indexPattern?: IIndexPattern | null) {
+export function filterMatchesIndex(filter: Filter, indexPattern?: IndexPatternBase | null) {
if (!filter.meta?.key || !indexPattern) {
return true;
}
diff --git a/src/plugins/data/common/es_query/es_query/from_filters.ts b/src/plugins/data/common/es_query/es_query/from_filters.ts
index e50862235af1d..7b3c58d45a569 100644
--- a/src/plugins/data/common/es_query/es_query/from_filters.ts
+++ b/src/plugins/data/common/es_query/es_query/from_filters.ts
@@ -10,7 +10,7 @@ import { isUndefined } from 'lodash';
import { migrateFilter } from './migrate_filter';
import { filterMatchesIndex } from './filter_matches_index';
import { Filter, cleanFilter, isFilterDisabled } from '../filters';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
import { handleNestedFilter } from './handle_nested_filter';
/**
@@ -45,7 +45,7 @@ const translateToQuery = (filter: Filter) => {
export const buildQueryFromFilters = (
filters: Filter[] = [],
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
ignoreFilterIfFieldNotInIndex: boolean = false
) => {
filters = filters.filter((filter) => filter && !isFilterDisabled(filter));
diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.ts b/src/plugins/data/common/es_query/es_query/from_kuery.ts
index afedaae45872b..3eccfd8776113 100644
--- a/src/plugins/data/common/es_query/es_query/from_kuery.ts
+++ b/src/plugins/data/common/es_query/es_query/from_kuery.ts
@@ -7,11 +7,11 @@
*/
import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '../kuery';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
import { Query } from '../../query/types';
export function buildQueryFromKuery(
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
queries: Query[] = [],
allowLeadingWildcards: boolean = false,
dateFormatTZ?: string
@@ -24,7 +24,7 @@ export function buildQueryFromKuery(
}
function buildQuery(
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
queryASTs: KueryNode[],
config: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts b/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts
index ee5305132042a..d312d034df564 100644
--- a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts
+++ b/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts
@@ -9,13 +9,14 @@
import { handleNestedFilter } from './handle_nested_filter';
import { fields } from '../../index_patterns/mocks';
import { buildPhraseFilter, buildQueryFilter } from '../filters';
-import { IFieldType, IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
+import { IFieldType } from '../../index_patterns';
describe('handleNestedFilter', function () {
- const indexPattern: IIndexPattern = ({
+ const indexPattern: IndexPatternBase = {
id: 'logstash-*',
fields,
- } as unknown) as IIndexPattern;
+ };
it("should return the filter's query wrapped in nested query if the target field is nested", () => {
const field = getField('nestedField.child');
diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts b/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts
index 93927d81565ef..60e92769503fb 100644
--- a/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts
+++ b/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts
@@ -7,9 +7,9 @@
*/
import { getFilterField, cleanFilter, Filter } from '../filters';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
-export const handleNestedFilter = (filter: Filter, indexPattern?: IIndexPattern) => {
+export const handleNestedFilter = (filter: Filter, indexPattern?: IndexPatternBase) => {
if (!indexPattern) return filter;
const fieldName = getFilterField(filter);
diff --git a/src/plugins/data/common/es_query/es_query/index.ts b/src/plugins/data/common/es_query/es_query/index.ts
index 31529480c8ac9..c10ea5846ae3f 100644
--- a/src/plugins/data/common/es_query/es_query/index.ts
+++ b/src/plugins/data/common/es_query/es_query/index.ts
@@ -11,3 +11,4 @@ export { buildQueryFromFilters } from './from_filters';
export { luceneStringToDsl } from './lucene_string_to_dsl';
export { decorateQuery } from './decorate_query';
export { getEsQueryConfig } from './get_es_query_config';
+export { IndexPatternBase } from './types';
diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.ts
index c7c44d019a31c..9bd78b092fc18 100644
--- a/src/plugins/data/common/es_query/es_query/migrate_filter.ts
+++ b/src/plugins/data/common/es_query/es_query/migrate_filter.ts
@@ -9,7 +9,7 @@
import { get, omit } from 'lodash';
import { getConvertedValueForField } from '../filters';
import { Filter } from '../filters';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
export interface DeprecatedMatchPhraseFilter extends Filter {
query: {
@@ -28,7 +28,7 @@ function isDeprecatedMatchPhraseFilter(filter: any): filter is DeprecatedMatchPh
return Boolean(fieldName && get(filter, ['query', 'match', fieldName, 'type']) === 'phrase');
}
-export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) {
+export function migrateFilter(filter: Filter, indexPattern?: IndexPatternBase) {
if (isDeprecatedMatchPhraseFilter(filter)) {
const fieldName = Object.keys(filter.query.match)[0];
const params: Record = get(filter, ['query', 'match', fieldName]);
diff --git a/packages/kbn-interpreter/src/common/index.d.ts b/src/plugins/data/common/es_query/es_query/types.ts
similarity index 67%
rename from packages/kbn-interpreter/src/common/index.d.ts
rename to src/plugins/data/common/es_query/es_query/types.ts
index 6f54d07590973..2133736516049 100644
--- a/packages/kbn-interpreter/src/common/index.d.ts
+++ b/src/plugins/data/common/es_query/es_query/types.ts
@@ -6,7 +6,9 @@
* Side Public License, v 1.
*/
-export { Registry } from './lib/registry';
+import { IFieldType } from '../../index_patterns';
-export { fromExpression, toExpression, Ast, ExpressionFunctionAST } from './lib/ast';
-export { getType } from './lib/get_type';
+export interface IndexPatternBase {
+ fields: IFieldType[];
+ id?: string;
+}
diff --git a/src/plugins/data/common/es_query/filters/build_filters.ts b/src/plugins/data/common/es_query/filters/build_filters.ts
index ba1bd0a615493..369f9530fb92b 100644
--- a/src/plugins/data/common/es_query/filters/build_filters.ts
+++ b/src/plugins/data/common/es_query/filters/build_filters.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { IIndexPattern, IFieldType } from '../..';
+import { IFieldType, IndexPatternBase } from '../..';
import {
Filter,
FILTERS,
@@ -19,7 +19,7 @@ import {
} from '.';
export function buildFilter(
- indexPattern: IIndexPattern,
+ indexPattern: IndexPatternBase,
field: IFieldType,
type: FILTERS,
negate: boolean,
@@ -59,7 +59,7 @@ export function buildCustomFilter(
}
function buildBaseFilter(
- indexPattern: IIndexPattern,
+ indexPattern: IndexPatternBase,
field: IFieldType,
type: FILTERS,
params: any
diff --git a/src/plugins/data/common/es_query/filters/exists_filter.ts b/src/plugins/data/common/es_query/filters/exists_filter.ts
index 441a6bcb924b7..4836950c3bb27 100644
--- a/src/plugins/data/common/es_query/filters/exists_filter.ts
+++ b/src/plugins/data/common/es_query/filters/exists_filter.ts
@@ -7,7 +7,8 @@
*/
import { Filter, FilterMeta } from './meta_filter';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '..';
export type ExistsFilterMeta = FilterMeta;
@@ -26,7 +27,7 @@ export const getExistsFilterField = (filter: ExistsFilter) => {
return filter.exists && filter.exists.field;
};
-export const buildExistsFilter = (field: IFieldType, indexPattern: IIndexPattern) => {
+export const buildExistsFilter = (field: IFieldType, indexPattern: IndexPatternBase) => {
return {
meta: {
index: indexPattern.id,
diff --git a/src/plugins/data/common/es_query/filters/index.ts b/src/plugins/data/common/es_query/filters/index.ts
index 133f5cd232e6f..fe7cdadabaee3 100644
--- a/src/plugins/data/common/es_query/filters/index.ts
+++ b/src/plugins/data/common/es_query/filters/index.ts
@@ -14,10 +14,8 @@ export * from './custom_filter';
export * from './exists_filter';
export * from './geo_bounding_box_filter';
export * from './geo_polygon_filter';
-export * from './get_display_value';
export * from './get_filter_field';
export * from './get_filter_params';
-export * from './get_index_pattern_from_filter';
export * from './match_all_filter';
export * from './meta_filter';
export * from './missing_filter';
diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.ts b/src/plugins/data/common/es_query/filters/phrase_filter.ts
index 85562435e68d0..27c1e85562097 100644
--- a/src/plugins/data/common/es_query/filters/phrase_filter.ts
+++ b/src/plugins/data/common/es_query/filters/phrase_filter.ts
@@ -8,7 +8,8 @@
import type { estypes } from '@elastic/elasticsearch';
import { get, isPlainObject } from 'lodash';
import { Filter, FilterMeta } from './meta_filter';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '..';
export type PhraseFilterMeta = FilterMeta & {
params?: {
@@ -60,7 +61,7 @@ export const getPhraseFilterValue = (filter: PhraseFilter): PhraseFilterValue =>
export const buildPhraseFilter = (
field: IFieldType,
value: any,
- indexPattern: IIndexPattern
+ indexPattern: IndexPatternBase
): PhraseFilter => {
const convertedValue = getConvertedValueForField(field, value);
diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.ts b/src/plugins/data/common/es_query/filters/phrases_filter.ts
index 849c1b3faef2a..8a79472154493 100644
--- a/src/plugins/data/common/es_query/filters/phrases_filter.ts
+++ b/src/plugins/data/common/es_query/filters/phrases_filter.ts
@@ -9,7 +9,8 @@
import { Filter, FilterMeta } from './meta_filter';
import { getPhraseScript } from './phrase_filter';
import { FILTERS } from './index';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '../es_query';
export type PhrasesFilterMeta = FilterMeta & {
params: string[]; // The unformatted values
@@ -34,7 +35,7 @@ export const getPhrasesFilterField = (filter: PhrasesFilter) => {
export const buildPhrasesFilter = (
field: IFieldType,
params: any[],
- indexPattern: IIndexPattern
+ indexPattern: IndexPatternBase
) => {
const index = indexPattern.id;
const type = FILTERS.PHRASES;
diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts
index a082b93c0a79a..7bc7a8cff7487 100644
--- a/src/plugins/data/common/es_query/filters/range_filter.ts
+++ b/src/plugins/data/common/es_query/filters/range_filter.ts
@@ -8,7 +8,8 @@
import type { estypes } from '@elastic/elasticsearch';
import { map, reduce, mapValues, get, keys, pickBy } from 'lodash';
import { Filter, FilterMeta } from './meta_filter';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '..';
const OPERANDS_IN_RANGE = 2;
@@ -93,7 +94,7 @@ const format = (field: IFieldType, value: any) =>
export const buildRangeFilter = (
field: IFieldType,
params: RangeFilterParams,
- indexPattern: IIndexPattern,
+ indexPattern: IndexPatternBase,
formattedValue?: string
): RangeFilter => {
const filter: any = { meta: { index: indexPattern.id, params: {} } };
diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.ts b/src/plugins/data/common/es_query/kuery/ast/ast.ts
index be82128969968..3e7b25897cab7 100644
--- a/src/plugins/data/common/es_query/kuery/ast/ast.ts
+++ b/src/plugins/data/common/es_query/kuery/ast/ast.ts
@@ -10,10 +10,10 @@ import { JsonObject } from '@kbn/common-utils';
import { nodeTypes } from '../node_types/index';
import { KQLSyntaxError } from '../kuery_syntax_error';
import { KueryNode, DslQuery, KueryParseOptions } from '../types';
-import { IIndexPattern } from '../../../index_patterns/types';
// @ts-ignore
import { parse as parseKuery } from './_generated_/kuery';
+import { IndexPatternBase } from '../..';
const fromExpression = (
expression: string | DslQuery,
@@ -65,7 +65,7 @@ export const fromKueryExpression = (
*/
export const toElasticsearchQuery = (
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config?: Record,
context?: Record
): JsonObject => {
diff --git a/src/plugins/data/common/es_query/kuery/functions/and.ts b/src/plugins/data/common/es_query/kuery/functions/and.ts
index 1989704cb627e..ba7d5d1f6645b 100644
--- a/src/plugins/data/common/es_query/kuery/functions/and.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/and.ts
@@ -7,7 +7,7 @@
*/
import * as ast from '../ast';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(children: KueryNode[]) {
return {
@@ -17,7 +17,7 @@ export function buildNodeParams(children: KueryNode[]) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/exists.ts b/src/plugins/data/common/es_query/kuery/functions/exists.ts
index 5238fb1d8ee7f..fa6c37e6ba18f 100644
--- a/src/plugins/data/common/es_query/kuery/functions/exists.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/exists.ts
@@ -8,7 +8,7 @@
import { get } from 'lodash';
import * as literal from '../node_types/literal';
-import { IIndexPattern, KueryNode, IFieldType } from '../../..';
+import { KueryNode, IFieldType, IndexPatternBase } from '../../..';
export function buildNodeParams(fieldName: string) {
return {
@@ -18,7 +18,7 @@ export function buildNodeParams(fieldName: string) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts
index f2498f3ea2ad4..38a433b1b80ab 100644
--- a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts
@@ -9,7 +9,7 @@
import _ from 'lodash';
import { nodeTypes } from '../node_types';
import * as ast from '../ast';
-import { IIndexPattern, KueryNode, IFieldType, LatLon } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType, LatLon } from '../../..';
export function buildNodeParams(fieldName: string, params: any) {
params = _.pick(params, 'topLeft', 'bottomRight');
@@ -26,7 +26,7 @@ export function buildNodeParams(fieldName: string, params: any) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts
index 584a315930d9c..69de7248a7b38 100644
--- a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts
@@ -8,7 +8,7 @@
import { nodeTypes } from '../node_types';
import * as ast from '../ast';
-import { IIndexPattern, KueryNode, IFieldType, LatLon } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType, LatLon } from '../../..';
import { LiteralTypeBuildNode } from '../node_types/types';
export function buildNodeParams(fieldName: string, points: LatLon[]) {
@@ -25,7 +25,7 @@ export function buildNodeParams(fieldName: string, points: LatLon[]) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/is.ts b/src/plugins/data/common/es_query/kuery/functions/is.ts
index a18ad230c3cae..55d036c2156f9 100644
--- a/src/plugins/data/common/es_query/kuery/functions/is.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/is.ts
@@ -11,7 +11,7 @@ import { getPhraseScript } from '../../filters';
import { getFields } from './utils/get_fields';
import { getTimeZoneFromSettings } from '../../utils';
import { getFullFieldNameNode } from './utils/get_full_field_name_node';
-import { IIndexPattern, KueryNode, IFieldType } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType } from '../../..';
import * as ast from '../ast';
@@ -39,7 +39,7 @@ export function buildNodeParams(fieldName: string, value: any, isPhrase: boolean
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/nested.ts b/src/plugins/data/common/es_query/kuery/functions/nested.ts
index bfd01ef39764c..46ceeaf3e5de6 100644
--- a/src/plugins/data/common/es_query/kuery/functions/nested.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/nested.ts
@@ -8,7 +8,7 @@
import * as ast from '../ast';
import * as literal from '../node_types/literal';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(path: any, child: any) {
const pathNode =
@@ -20,7 +20,7 @@ export function buildNodeParams(path: any, child: any) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/not.ts b/src/plugins/data/common/es_query/kuery/functions/not.ts
index ef4456897bcdd..f837cd261c814 100644
--- a/src/plugins/data/common/es_query/kuery/functions/not.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/not.ts
@@ -7,7 +7,7 @@
*/
import * as ast from '../ast';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(child: KueryNode) {
return {
@@ -17,7 +17,7 @@ export function buildNodeParams(child: KueryNode) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/or.ts b/src/plugins/data/common/es_query/kuery/functions/or.ts
index 416687e7cde9c..7365cc39595e6 100644
--- a/src/plugins/data/common/es_query/kuery/functions/or.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/or.ts
@@ -7,7 +7,7 @@
*/
import * as ast from '../ast';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(children: KueryNode[]) {
return {
@@ -17,7 +17,7 @@ export function buildNodeParams(children: KueryNode[]) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/range.ts b/src/plugins/data/common/es_query/kuery/functions/range.ts
index 06b345e5821c3..caefa7e5373ca 100644
--- a/src/plugins/data/common/es_query/kuery/functions/range.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/range.ts
@@ -13,7 +13,7 @@ import { getRangeScript, RangeFilterParams } from '../../filters';
import { getFields } from './utils/get_fields';
import { getTimeZoneFromSettings } from '../../utils';
import { getFullFieldNameNode } from './utils/get_full_field_name_node';
-import { IIndexPattern, KueryNode, IFieldType } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType } from '../../..';
export function buildNodeParams(fieldName: string, params: RangeFilterParams) {
const paramsToMap = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format');
@@ -33,7 +33,7 @@ export function buildNodeParams(fieldName: string, params: RangeFilterParams) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts
index 4002a36648f04..7dac1262d5062 100644
--- a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts
@@ -8,10 +8,10 @@
import * as literal from '../../node_types/literal';
import * as wildcard from '../../node_types/wildcard';
-import { KueryNode, IIndexPattern } from '../../../..';
+import { KueryNode, IndexPatternBase } from '../../../..';
import { LiteralTypeBuildNode } from '../../node_types/types';
-export function getFields(node: KueryNode, indexPattern?: IIndexPattern) {
+export function getFields(node: KueryNode, indexPattern?: IndexPatternBase) {
if (!indexPattern) return [];
if (node.type === 'literal') {
const fieldName = literal.toElasticsearchQuery(node as LiteralTypeBuildNode);
diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts
index e623579226861..644791637aa70 100644
--- a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts
@@ -7,11 +7,11 @@
*/
import { getFields } from './get_fields';
-import { IIndexPattern, IFieldType, KueryNode } from '../../../..';
+import { IndexPatternBase, IFieldType, KueryNode } from '../../../..';
export function getFullFieldNameNode(
rootNameNode: any,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
nestedPath?: string
): KueryNode {
const fullFieldNameNode = {
diff --git a/src/plugins/data/common/es_query/kuery/node_types/function.ts b/src/plugins/data/common/es_query/kuery/node_types/function.ts
index b9b7379dfb23d..642089a101f31 100644
--- a/src/plugins/data/common/es_query/kuery/node_types/function.ts
+++ b/src/plugins/data/common/es_query/kuery/node_types/function.ts
@@ -9,7 +9,7 @@
import _ from 'lodash';
import { functions } from '../functions';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
import { FunctionName, FunctionTypeBuildNode } from './types';
export function buildNode(functionName: FunctionName, ...args: any[]) {
@@ -45,7 +45,7 @@ export function buildNodeWithArgumentNodes(
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config?: Record,
context?: Record
) {
diff --git a/src/plugins/data/common/es_query/kuery/node_types/types.ts b/src/plugins/data/common/es_query/kuery/node_types/types.ts
index b3247a0ad8dc2..ea8eb5e8a0618 100644
--- a/src/plugins/data/common/es_query/kuery/node_types/types.ts
+++ b/src/plugins/data/common/es_query/kuery/node_types/types.ts
@@ -11,8 +11,8 @@
*/
import { JsonValue } from '@kbn/common-utils';
-import { IIndexPattern } from '../../../index_patterns';
import { KueryNode } from '..';
+import { IndexPatternBase } from '../..';
export type FunctionName =
| 'is'
@@ -30,7 +30,7 @@ interface FunctionType {
buildNodeWithArgumentNodes: (functionName: FunctionName, args: any[]) => FunctionTypeBuildNode;
toElasticsearchQuery: (
node: any,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config?: Record,
context?: Record
) => JsonValue;
diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts
index 07aa8967b905e..a88f029c0c7cd 100644
--- a/src/plugins/data/common/index_patterns/types.ts
+++ b/src/plugins/data/common/index_patterns/types.ts
@@ -9,6 +9,7 @@ import type { estypes } from '@elastic/elasticsearch';
import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications';
// eslint-disable-next-line
import type { SavedObject } from 'src/core/server';
+import type { IndexPatternBase } from '../es_query';
import { IFieldType } from './fields';
import { RUNTIME_FIELD_TYPES } from './constants';
import { SerializedFieldFormat } from '../../../expressions/common';
@@ -29,10 +30,8 @@ export interface RuntimeField {
* IIndexPattern allows for an IndexPattern OR an index pattern saved object
* Use IndexPattern or IndexPatternSpec instead
*/
-export interface IIndexPattern {
- fields: IFieldType[];
+export interface IIndexPattern extends IndexPatternBase {
title: string;
- id?: string;
/**
* Type is used for identifying rollup indices, otherwise left undefined
*/
diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts
index 3c83b5bdf6084..9a35cf983c805 100644
--- a/src/plugins/data/common/search/aggs/agg_config.ts
+++ b/src/plugins/data/common/search/aggs/agg_config.ts
@@ -192,9 +192,8 @@ export class AggConfig {
} else if (!this.aggConfigs.timeRange) {
return;
}
- return moment.duration(
- moment(this.aggConfigs.timeRange.to).diff(this.aggConfigs.timeRange.from)
- );
+ const resolvedBounds = this.aggConfigs.getResolvedTimeRange()!;
+ return moment.duration(moment(resolvedBounds.max).diff(resolvedBounds.min));
}
return parsedTimeShift;
}
diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts
index 8593a0b0ed0fa..c205b46e077f0 100644
--- a/src/plugins/data/common/search/aggs/agg_configs.ts
+++ b/src/plugins/data/common/search/aggs/agg_configs.ts
@@ -23,7 +23,7 @@ import { IAggType } from './agg_type';
import { AggTypesRegistryStart } from './agg_types_registry';
import { AggGroupNames } from './agg_groups';
import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern';
-import { TimeRange, getTime, isRangeFilter } from '../../../common';
+import { TimeRange, getTime, isRangeFilter, calculateBounds } from '../../../common';
import { IBucketAggConfig } from './buckets';
import { insertTimeShiftSplit, mergeTimeShifts } from './utils/time_splits';
@@ -127,6 +127,19 @@ export class AggConfigs {
this.aggs.forEach(updateAggTimeRange);
}
+ /**
+ * Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set)
+ * @returns Current time range as resolved date.
+ */
+ getResolvedTimeRange() {
+ return (
+ this.timeRange &&
+ calculateBounds(this.timeRange, {
+ forceNow: this.forceNow,
+ })
+ );
+ }
+
// clone method will reuse existing AggConfig in the list (will not create new instances)
clone({ enabledOnly = true } = {}) {
const filterAggs = (agg: AggConfig) => {
diff --git a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts
index 4d8ee0f889173..91379ea054de3 100644
--- a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts
+++ b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts
@@ -20,7 +20,7 @@ export const parseTimeShift = (val: string): moment.Duration | 'previous' | 'inv
if (trimmedVal === 'previous') {
return 'previous';
}
- const [, amount, unit] = trimmedVal.match(/^(\d+)(\w)$/) || [];
+ const [, amount, unit] = trimmedVal.match(/^(\d+)\s*(\w)$/) || [];
const parsedAmount = Number(amount);
if (Number.isNaN(parsedAmount) || !allowedUnits.includes(unit as AllowedUnit)) {
return 'invalid';
diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts
index d1890ec97df4e..c5cf3f9f09e6c 100644
--- a/src/plugins/data/common/search/types.ts
+++ b/src/plugins/data/common/search/types.ts
@@ -65,6 +65,11 @@ export interface IKibanaSearchResponse {
*/
isPartial?: boolean;
+ /**
+ * Indicates whether the results returned are from the async-search index
+ */
+ isRestored?: boolean;
+
/**
* The raw response returned by the internal search method (usually the raw ES response)
*/
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index 078dd3a9b7c5a..d7667f20d517e 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -23,7 +23,6 @@ import {
disableFilter,
FILTERS,
FilterStateStore,
- getDisplayValueFromFilter,
getPhraseFilterField,
getPhraseFilterValue,
isExistsFilter,
@@ -43,6 +42,7 @@ import { FilterLabel } from './ui';
import { FilterItem } from './ui/filter_bar';
import {
+ getDisplayValueFromFilter,
generateFilters,
onlyDisabledFiltersChanged,
changeTimeFilter,
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index d56727b468da6..2849b93b14483 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -259,6 +259,7 @@ export class AggConfigs {
getRequestAggById(id: string): AggConfig | undefined;
// (undocumented)
getRequestAggs(): AggConfig[];
+ getResolvedTimeRange(): import("../..").TimeRangeBounds | undefined;
getResponseAggById(id: string): AggConfig | undefined;
getResponseAggs(): AggConfig[];
// (undocumented)
@@ -807,11 +808,11 @@ export const esFilters: {
FILTERS: typeof FILTERS;
FilterStateStore: typeof FilterStateStore;
buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter;
- buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter;
- buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter;
- buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter;
+ buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter;
+ buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter;
+ buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter;
buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter;
- buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
+ buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter;
isPhraseFilter: (filter: any) => filter is import("../common").PhraseFilter;
isExistsFilter: (filter: any) => filter is import("../common").ExistsFilter;
isPhrasesFilter: (filter: any) => filter is import("../common").PhrasesFilter;
@@ -857,7 +858,7 @@ export const esFilters: {
export const esKuery: {
nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes;
fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode;
- toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
+ toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
};
// Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -866,7 +867,7 @@ export const esKuery: {
export const esQuery: {
buildEsQuery: typeof buildEsQuery;
getEsQueryConfig: typeof getEsQueryConfig;
- buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
+ buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
must: never[];
filter: import("../common").Filter[];
should: never[];
@@ -1285,22 +1286,19 @@ export interface IFieldType {
visualizable?: boolean;
}
+// Warning: (ae-forgotten-export) The symbol "IndexPatternBase" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public @deprecated (undocumented)
-export interface IIndexPattern {
+export interface IIndexPattern extends IndexPatternBase {
// Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts
//
// (undocumented)
fieldFormatMap?: Record | undefined>;
- // (undocumented)
- fields: IFieldType[];
getFormatterForField?: (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat;
// (undocumented)
getTimeField?(): IFieldType | undefined;
// (undocumented)
- id?: string;
- // (undocumented)
timeFieldName?: string;
// (undocumented)
title: string;
@@ -1350,6 +1348,7 @@ export interface IKibanaSearchRequest {
export interface IKibanaSearchResponse {
id?: string;
isPartial?: boolean;
+ isRestored?: boolean;
isRunning?: boolean;
loaded?: number;
rawResponse: RawResponse;
@@ -2729,13 +2728,13 @@ export interface WaitUntilNextSessionCompletesOptions {
// Warnings were encountered during analysis:
//
-// src/plugins/data/common/es_query/filters/exists_filter.ts:19:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/exists_filter.ts:21:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/match_all_filter.ts:17:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/meta_filter.ts:44:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/phrase_filter.ts:22:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/phrases_filter.ts:20:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/phrase_filter.ts:23:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/phrases_filter.ts:21:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/data/public/query/filter_manager/index.ts b/src/plugins/data/public/query/filter_manager/index.ts
index 327b9763541ac..55dba640b07b6 100644
--- a/src/plugins/data/public/query/filter_manager/index.ts
+++ b/src/plugins/data/public/query/filter_manager/index.ts
@@ -11,3 +11,5 @@ export { FilterManager } from './filter_manager';
export { mapAndFlattenFilters } from './lib/map_and_flatten_filters';
export { onlyDisabledFiltersChanged } from './lib/only_disabled';
export { generateFilters } from './lib/generate_filters';
+export { getDisplayValueFromFilter } from './lib/get_display_value';
+export { getIndexPatternFromFilter } from './lib/get_index_pattern_from_filter';
diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts
similarity index 95%
rename from src/plugins/data/common/es_query/filters/get_display_value.ts
rename to src/plugins/data/public/query/filter_manager/lib/get_display_value.ts
index ee719843ae879..45c6167f600bc 100644
--- a/src/plugins/data/common/es_query/filters/get_display_value.ts
+++ b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts
@@ -7,9 +7,8 @@
*/
import { i18n } from '@kbn/i18n';
-import { IIndexPattern } from '../..';
+import { Filter, IIndexPattern } from '../../../../common';
import { getIndexPatternFromFilter } from './get_index_pattern_from_filter';
-import { Filter } from '../filters';
function getValueFormatter(indexPattern?: IIndexPattern, key?: string) {
// checking getFormatterForField exists because there is at least once case where an index pattern
diff --git a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.test.ts
similarity index 100%
rename from src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts
rename to src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.test.ts
diff --git a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts
similarity index 88%
rename from src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts
rename to src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts
index bceeb5f2793ec..7a2ce29102e51 100644
--- a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts
+++ b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts
@@ -6,8 +6,7 @@
* Side Public License, v 1.
*/
-import { Filter } from '../filters';
-import { IIndexPattern } from '../..';
+import { Filter, IIndexPattern } from '../../../../common';
export function getIndexPatternFromFilter(
filter: Filter,
diff --git a/src/plugins/data/public/search/errors/index.ts b/src/plugins/data/public/search/errors/index.ts
index 82c9e04b79798..fcdea8dec1c2e 100644
--- a/src/plugins/data/public/search/errors/index.ts
+++ b/src/plugins/data/public/search/errors/index.ts
@@ -12,3 +12,4 @@ export * from './timeout_error';
export * from './utils';
export * from './types';
export * from './http_error';
+export * from './search_session_incomplete_warning';
diff --git a/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx
new file mode 100644
index 0000000000000..c5c5c37f31cf8
--- /dev/null
+++ b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+import { CoreStart } from 'kibana/public';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export const SearchSessionIncompleteWarning = (docLinks: CoreStart['docLinks']) => (
+ <>
+
+ It needs more time to fully render. You can wait here or come back to it later.
+
+
+
+
+
+
+ >
+);
diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
index fe66d4b6e9937..155638250a2a4 100644
--- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
+++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
@@ -29,6 +29,12 @@ jest.mock('./utils', () => ({
}),
}));
+jest.mock('../errors/search_session_incomplete_warning', () => ({
+ SearchSessionIncompleteWarning: jest.fn(),
+}));
+
+import { SearchSessionIncompleteWarning } from '../errors/search_session_incomplete_warning';
+
let searchInterceptor: SearchInterceptor;
let mockCoreSetup: MockedKeys;
let bfetchSetup: jest.Mocked;
@@ -508,6 +514,7 @@ describe('SearchInterceptor', () => {
}
: null
);
+ sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore);
fetchMock.mockResolvedValue({ result: 200 });
};
@@ -562,6 +569,92 @@ describe('SearchInterceptor', () => {
(sessionService as jest.Mocked).getSearchOptions
).toHaveBeenCalledWith(sessionId);
});
+
+ test('should not show warning if a search is available during restore', async () => {
+ setup({
+ isRestore: true,
+ isStored: true,
+ sessionId: '123',
+ });
+
+ const responses = [
+ {
+ time: 10,
+ value: {
+ isPartial: false,
+ isRunning: false,
+ isRestored: true,
+ id: 1,
+ rawResponse: {
+ took: 1,
+ },
+ },
+ },
+ ];
+ mockFetchImplementation(responses);
+
+ const response = searchInterceptor.search(
+ {},
+ {
+ sessionId: '123',
+ }
+ );
+ response.subscribe({ next, error, complete });
+
+ await timeTravel(10);
+
+ expect(SearchSessionIncompleteWarning).toBeCalledTimes(0);
+ });
+
+ test('should show warning once if a search is not available during restore', async () => {
+ setup({
+ isRestore: true,
+ isStored: true,
+ sessionId: '123',
+ });
+
+ const responses = [
+ {
+ time: 10,
+ value: {
+ isPartial: false,
+ isRunning: false,
+ isRestored: false,
+ id: 1,
+ rawResponse: {
+ took: 1,
+ },
+ },
+ },
+ ];
+ mockFetchImplementation(responses);
+
+ searchInterceptor
+ .search(
+ {},
+ {
+ sessionId: '123',
+ }
+ )
+ .subscribe({ next, error, complete });
+
+ await timeTravel(10);
+
+ expect(SearchSessionIncompleteWarning).toBeCalledTimes(1);
+
+ searchInterceptor
+ .search(
+ {},
+ {
+ sessionId: '123',
+ }
+ )
+ .subscribe({ next, error, complete });
+
+ await timeTravel(10);
+
+ expect(SearchSessionIncompleteWarning).toBeCalledTimes(1);
+ });
});
describe('Session tracking', () => {
diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
index 57b156a9b3c00..e0e1df65101c7 100644
--- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
@@ -43,6 +43,7 @@ import {
PainlessError,
SearchTimeoutError,
TimeoutErrorMode,
+ SearchSessionIncompleteWarning,
} from '../errors';
import { toMountPoint } from '../../../../kibana_react/public';
import { AbortError, KibanaServerError } from '../../../../kibana_utils/public';
@@ -82,6 +83,7 @@ export class SearchInterceptor {
* @internal
*/
private application!: CoreStart['application'];
+ private docLinks!: CoreStart['docLinks'];
private batchedFetch!: BatchedFunc<
{ request: IKibanaSearchRequest; options: ISearchOptionsSerializable },
IKibanaSearchResponse
@@ -95,6 +97,7 @@ export class SearchInterceptor {
this.deps.startServices.then(([coreStart]) => {
this.application = coreStart.application;
+ this.docLinks = coreStart.docLinks;
});
this.batchedFetch = deps.bfetch.batchedFunction({
@@ -345,6 +348,11 @@ export class SearchInterceptor {
this.handleSearchError(e, searchOptions, searchAbortController.isTimeout())
);
}),
+ tap((response) => {
+ if (this.deps.session.isRestore() && response.isRestored === false) {
+ this.showRestoreWarning(this.deps.session.getSessionId());
+ }
+ }),
finalize(() => {
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
if (untrackSearch && this.deps.session.isCurrentSession(sessionId)) {
@@ -371,6 +379,25 @@ export class SearchInterceptor {
}
);
+ private showRestoreWarningToast = (sessionId?: string) => {
+ this.deps.toasts.addWarning(
+ {
+ title: 'Your search session is still running',
+ text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks)),
+ },
+ {
+ toastLifeTimeMs: 60000,
+ }
+ );
+ };
+
+ private showRestoreWarning = memoize(
+ this.showRestoreWarningToast,
+ (_: SearchTimeoutError, sessionId: string) => {
+ return sessionId;
+ }
+ );
+
/**
* Show one error notification per session.
* @internal
diff --git a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx
index 23de8327ce1f1..9cc9af04409f1 100644
--- a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx
+++ b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx
@@ -20,9 +20,9 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component } from 'react';
import { IIndexPattern } from '../..';
-import { getDisplayValueFromFilter, Filter } from '../../../common';
+import { Filter } from '../../../common';
import { FilterLabel } from '../filter_bar';
-import { mapAndFlattenFilters } from '../../query';
+import { mapAndFlattenFilters, getDisplayValueFromFilter } from '../../query';
interface Props {
filters: Filter[];
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
index 2b8978a125bca..734161ea87232 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
@@ -37,10 +37,10 @@ import { Operator } from './lib/filter_operators';
import { PhraseValueInput } from './phrase_value_input';
import { PhrasesValuesInput } from './phrases_values_input';
import { RangeValueInput } from './range_value_input';
+import { getIndexPatternFromFilter } from '../../../query';
import { IIndexPattern, IFieldType } from '../../..';
import {
Filter,
- getIndexPatternFromFilter,
FieldFilter,
buildFilter,
buildCustomFilter,
diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx
index 9e5090f945182..09e0571c2a870 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx
@@ -14,14 +14,13 @@ import { IUiSettingsClient } from 'src/core/public';
import { FilterEditor } from './filter_editor';
import { FilterView } from './filter_view';
import { IIndexPattern } from '../..';
+import { getDisplayValueFromFilter, getIndexPatternFromFilter } from '../../query';
import {
Filter,
isFilterPinned,
- getDisplayValueFromFilter,
toggleFilterNegated,
toggleFilterPinned,
toggleFilterDisabled,
- getIndexPatternFromFilter,
} from '../../../common';
import { getIndexPatterns } from '../../services';
diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap
index a0a7e54d27532..0ab3f8a4e3466 100644
--- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap
+++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap
@@ -176,27 +176,27 @@ exports[`Inspector Data View component should render empty state 1`] = `
+
+
+
+ No data available
+
+
+
-
-
-
- No data available
-
-
-
diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts
index 0764f4f441e42..dd60951e6d228 100644
--- a/src/plugins/data/server/index.ts
+++ b/src/plugins/data/server/index.ts
@@ -238,6 +238,7 @@ export {
DataRequestHandlerContext,
AsyncSearchResponse,
AsyncSearchStatusResponse,
+ NoSearchIdInSessionError,
} from './search';
// Search namespace
diff --git a/src/plugins/data/server/search/errors/no_search_id_in_session.ts b/src/plugins/data/server/search/errors/no_search_id_in_session.ts
new file mode 100644
index 0000000000000..b291df1cee5ba
--- /dev/null
+++ b/src/plugins/data/server/search/errors/no_search_id_in_session.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { KbnError } from '../../../../kibana_utils/common';
+
+export class NoSearchIdInSessionError extends KbnError {
+ constructor() {
+ super('No search ID in this session matching the given search request');
+ }
+}
diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts
index 812f3171aef99..b9affe96ea2dd 100644
--- a/src/plugins/data/server/search/index.ts
+++ b/src/plugins/data/server/search/index.ts
@@ -13,3 +13,4 @@ export * from './strategies/eql_search';
export { usageProvider, SearchUsage, searchUsageObserver } from './collectors';
export * from './aggs';
export * from './session';
+export * from './errors/no_search_id_in_session';
diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts
index 52ee8e60a5b26..314cb2c3acbf8 100644
--- a/src/plugins/data/server/search/search_service.test.ts
+++ b/src/plugins/data/server/search/search_service.test.ts
@@ -25,6 +25,7 @@ import {
ISearchSessionService,
ISearchStart,
ISearchStrategy,
+ NoSearchIdInSessionError,
} from '.';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { expressionsPluginMock } from '../../../expressions/public/mocks';
@@ -175,6 +176,22 @@ describe('Search service', () => {
expect(request).toStrictEqual({ ...searchRequest, id: 'my_id' });
});
+ it('searches even if id is not found in session during restore', async () => {
+ const searchRequest = { params: {} };
+ const options = { sessionId, isStored: true, isRestore: true };
+
+ mockSessionClient.getId = jest.fn().mockImplementation(() => {
+ throw new NoSearchIdInSessionError();
+ });
+
+ const res = await mockScopedClient.search(searchRequest, options).toPromise();
+
+ const [request, callOptions] = mockStrategy.search.mock.calls[0];
+ expect(callOptions).toBe(options);
+ expect(request).toStrictEqual({ ...searchRequest });
+ expect(res.isRestored).toBe(false);
+ });
+
it('does not fail if `trackId` throws', async () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: false, isRestore: false };
diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts
index a651d7b3bf105..00dffefa5e3a6 100644
--- a/src/plugins/data/server/search/search_service.ts
+++ b/src/plugins/data/server/search/search_service.ts
@@ -19,7 +19,7 @@ import {
SharedGlobalConfig,
StartServicesAccessor,
} from 'src/core/server';
-import { first, switchMap, tap } from 'rxjs/operators';
+import { first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import type {
@@ -80,6 +80,7 @@ import { registerBsearchRoute } from './routes/bsearch';
import { getKibanaContext } from './expressions/kibana_context';
import { enhancedEsSearchStrategyProvider } from './strategies/ese_search';
import { eqlSearchStrategyProvider } from './strategies/eql_search';
+import { NoSearchIdInSessionError } from './errors/no_search_id_in_session';
type StrategyMap = Record>;
@@ -287,24 +288,48 @@ export class SearchService implements Plugin {
options.strategy
);
- const getSearchRequest = async () =>
- !options.sessionId || !options.isRestore || request.id
- ? request
- : {
+ const getSearchRequest = async () => {
+ if (!options.sessionId || !options.isRestore || request.id) {
+ return request;
+ } else {
+ try {
+ const id = await deps.searchSessionsClient.getId(request, options);
+ this.logger.debug(`Found search session id for request ${id}`);
+ return {
...request,
- id: await deps.searchSessionsClient.getId(request, options),
+ id,
};
+ } catch (e) {
+ if (e instanceof NoSearchIdInSessionError) {
+ this.logger.debug('Ignoring missing search ID');
+ return request;
+ } else {
+ throw e;
+ }
+ }
+ }
+ };
- return from(getSearchRequest()).pipe(
+ const searchRequest$ = from(getSearchRequest());
+ const search$ = searchRequest$.pipe(
switchMap((searchRequest) => strategy.search(searchRequest, options, deps)),
- tap((response) => {
- if (!options.sessionId || !response.id || options.isRestore) return;
+ withLatestFrom(searchRequest$),
+ tap(([response, requestWithId]) => {
+ if (!options.sessionId || !response.id || (options.isRestore && requestWithId.id)) return;
// intentionally swallow tracking error, as it shouldn't fail the search
deps.searchSessionsClient.trackId(request, response.id, options).catch((trackErr) => {
this.logger.error(trackErr);
});
+ }),
+ map(([response, requestWithId]) => {
+ return {
+ ...response,
+ isRestored: !!requestWithId.id,
+ };
})
);
+
+ return search$;
} catch (e) {
return throwError(e);
}
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index c2b533bc42dc6..5ca19f9e1e509 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -447,11 +447,11 @@ export const esFilters: {
buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter;
buildCustomFilter: typeof buildCustomFilter;
buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter;
- buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter;
+ buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter;
buildFilter: typeof buildFilter;
- buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter;
- buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter;
- buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
+ buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter;
+ buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter;
+ buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter;
isFilterDisabled: (filter: import("../common").Filter) => boolean;
};
@@ -461,14 +461,14 @@ export const esFilters: {
export const esKuery: {
nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes;
fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode;
- toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
+ toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
};
// Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const esQuery: {
- buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
+ buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
must: never[];
filter: import("../common").Filter[];
should: never[];
@@ -1205,6 +1205,14 @@ export enum METRIC_TYPES {
TOP_HITS = "top_hits"
}
+// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts
+// Warning: (ae-missing-release-tag) "NoSearchIdInSessionError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export class NoSearchIdInSessionError extends KbnError {
+ constructor();
+}
+
// Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -1537,18 +1545,18 @@ export function usageProvider(core: CoreSetup_2): SearchUsage;
// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/search/types.ts:115:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx
index 2fd394d98281b..57a9d518f838e 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 0430614d413b6..a10674323e5cb 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 0000000000000..8c32942740a76
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import React from 'react';
+import { EuiSelectable } from '@elastic/eui';
+import { ShallowWrapper } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import { shallowWithIntl } from '@kbn/test/jest';
+import { ChangeIndexPattern } from './change_indexpattern';
+import { indexPatternMock } from '../../../../../__mocks__/index_pattern';
+import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield';
+import { IndexPatternRef } from './types';
+
+function getProps() {
+ return {
+ indexPatternId: indexPatternMock.id,
+ indexPatternRefs: [
+ indexPatternMock as IndexPatternRef,
+ indexPatternWithTimefieldMock as IndexPatternRef,
+ ],
+ onChangeIndexPattern: jest.fn(),
+ trigger: {
+ label: indexPatternMock.title,
+ title: indexPatternMock.title,
+ 'data-test-subj': 'indexPattern-switch-link',
+ },
+ };
+}
+
+function getIndexPatternPickerList(instance: ShallowWrapper) {
+ return instance.find(EuiSelectable).first();
+}
+
+function getIndexPatternPickerOptions(instance: ShallowWrapper) {
+ return getIndexPatternPickerList(instance).prop('options');
+}
+
+export function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) {
+ const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions(
+ instance
+ ).map((option: { label: string }) =>
+ option.label === selectedLabel
+ ? { ...option, checked: 'on' }
+ : { ...option, checked: undefined }
+ );
+ return getIndexPatternPickerList(instance).prop('onChange')!(options);
+}
+
+describe('ChangeIndexPattern', () => {
+ test('switching index pattern to the same index pattern does not trigger onChangeIndexPattern', async () => {
+ const props = getProps();
+ const comp = shallowWithIntl( );
+ await act(async () => {
+ selectIndexPatternPickerOption(comp, indexPatternMock.title);
+ });
+ expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(0);
+ });
+ test('switching index pattern to a different index pattern triggers onChangeIndexPattern', async () => {
+ const props = getProps();
+ const comp = shallowWithIntl( );
+ await act(async () => {
+ selectIndexPatternPickerOption(comp, indexPatternWithTimefieldMock.title);
+ });
+ expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(1);
+ expect(props.onChangeIndexPattern).toHaveBeenCalledWith(indexPatternWithTimefieldMock.id);
+ });
+});
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx
index d5076e4daa990..5f2f35e2419dd 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx
@@ -26,17 +26,17 @@ export type ChangeIndexPatternTriggerProps = EuiButtonProps & {
// TODO: refactor to shared component with ../../../../../../../../x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern
export function ChangeIndexPattern({
- indexPatternRefs,
indexPatternId,
+ indexPatternRefs,
onChangeIndexPattern,
- trigger,
selectableProps,
+ trigger,
}: {
- trigger: ChangeIndexPatternTriggerProps;
+ indexPatternId?: string;
indexPatternRefs: IndexPatternRef[];
onChangeIndexPattern: (newId: string) => void;
- indexPatternId?: string;
selectableProps?: EuiSelectableProps<{ value: string }>;
+ trigger: ChangeIndexPatternTriggerProps;
}) {
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
@@ -86,7 +86,9 @@ export function ChangeIndexPattern({
const choice = (choices.find(({ checked }) => checked) as unknown) as {
value: string;
};
- onChangeIndexPattern(choice.value);
+ if (choice.value !== indexPatternId) {
+ onChangeIndexPattern(choice.value);
+ }
setPopoverIsOpen(false);
}}
searchProps={{
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx
index e60dabd1d8d8c..26a3c482e9d3c 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx
@@ -8,7 +8,7 @@
import './discover_field.scss';
-import React, { useState } from 'react';
+import React, { useState, useCallback, memo } from 'react';
import {
EuiPopover,
EuiPopoverTitle,
@@ -29,6 +29,172 @@ import { IndexPatternField, IndexPattern } from '../../../../../../../data/publi
import { getFieldTypeName } from './lib/get_field_type_name';
import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';
+function wrapOnDot(str?: string) {
+ // u200B is a non-width white-space character, which allows
+ // the browser to efficiently word-wrap right after the dot
+ // without us having to draw a lot of extra DOM elements, etc
+ return str ? str.replace(/\./g, '.\u200B') : '';
+}
+
+const FieldInfoIcon: React.FC = memo(() => (
+
+
+
+));
+
+const DiscoverFieldTypeIcon: React.FC<{ field: IndexPatternField }> = memo(({ field }) => (
+
+));
+
+const FieldName: React.FC<{ field: IndexPatternField }> = memo(({ field }) => {
+ const title =
+ field.displayName !== field.name
+ ? i18n.translate('discover.field.title', {
+ defaultMessage: '{fieldName} ({fieldDisplayName})',
+ values: {
+ fieldName: field.name,
+ fieldDisplayName: field.displayName,
+ },
+ })
+ : field.displayName;
+
+ return (
+
+ {wrapOnDot(field.displayName)}
+
+ );
+});
+
+interface ActionButtonProps {
+ field: IndexPatternField;
+ isSelected?: boolean;
+ alwaysShow: boolean;
+ toggleDisplay: (field: IndexPatternField) => void;
+}
+
+const ActionButton: React.FC = memo(
+ ({ field, isSelected, alwaysShow, toggleDisplay }) => {
+ const actionBtnClassName = classNames('dscSidebarItem__action', {
+ ['dscSidebarItem__mobile']: alwaysShow,
+ });
+ if (field.name === '_source') {
+ return null;
+ }
+ if (!isSelected) {
+ return (
+
+ ) => {
+ if (ev.type === 'click') {
+ ev.currentTarget.focus();
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ toggleDisplay(field);
+ }}
+ data-test-subj={`fieldToggle-${field.name}`}
+ aria-label={i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', {
+ defaultMessage: 'Add {field} to table',
+ values: { field: field.name },
+ })}
+ />
+
+ );
+ } else {
+ return (
+
+ ) => {
+ if (ev.type === 'click') {
+ ev.currentTarget.focus();
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ toggleDisplay(field);
+ }}
+ data-test-subj={`fieldToggle-${field.name}`}
+ aria-label={i18n.translate(
+ 'discover.fieldChooser.discoverField.removeButtonAriaLabel',
+ {
+ defaultMessage: 'Remove {field} from table',
+ values: { field: field.name },
+ }
+ )}
+ />
+
+ );
+ }
+ }
+);
+
+interface MultiFieldsProps {
+ multiFields: NonNullable;
+ toggleDisplay: (field: IndexPatternField) => void;
+ alwaysShowActionButton: boolean;
+}
+
+const MultiFields: React.FC = memo(
+ ({ multiFields, toggleDisplay, alwaysShowActionButton }) => (
+
+
+
+ {i18n.translate('discover.fieldChooser.discoverField.multiFields', {
+ defaultMessage: 'Multi fields',
+ })}
+
+
+ {multiFields.map((entry) => (
+ }
+ fieldAction={
+
+ }
+ fieldName={ }
+ key={entry.field.name}
+ />
+ ))}
+
+ )
+);
+
export interface DiscoverFieldProps {
/**
* Determines whether add/remove button is displayed not only when focused
@@ -85,7 +251,7 @@ export interface DiscoverFieldProps {
onDeleteField?: (fieldName: string) => void;
}
-export function DiscoverField({
+function DiscoverFieldComponent({
alwaysShowActionButton = false,
field,
indexPattern,
@@ -99,133 +265,22 @@ export function DiscoverField({
onEditField,
onDeleteField,
}: DiscoverFieldProps) {
- const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', {
- defaultMessage: 'Add {field} to table',
- values: { field: field.name },
- });
- const removeLabelAria = i18n.translate(
- 'discover.fieldChooser.discoverField.removeButtonAriaLabel',
- {
- defaultMessage: 'Remove {field} from table',
- values: { field: field.name },
- }
- );
-
const [infoIsOpen, setOpen] = useState(false);
- const toggleDisplay = (f: IndexPatternField, isSelected: boolean) => {
- if (isSelected) {
- onRemoveField(f.name);
- } else {
- onAddField(f.name);
- }
- };
+ const toggleDisplay = useCallback(
+ (f: IndexPatternField) => {
+ if (selected) {
+ onRemoveField(f.name);
+ } else {
+ onAddField(f.name);
+ }
+ },
+ [onAddField, onRemoveField, selected]
+ );
- function togglePopover() {
+ const togglePopover = useCallback(() => {
setOpen(!infoIsOpen);
- }
-
- function wrapOnDot(str?: string) {
- // u200B is a non-width white-space character, which allows
- // the browser to efficiently word-wrap right after the dot
- // without us having to draw a lot of extra DOM elements, etc
- return str ? str.replace(/\./g, '.\u200B') : '';
- }
-
- const getDscFieldIcon = (indexPatternField: IndexPatternField) => {
- return (
-
- );
- };
-
- const dscFieldIcon = getDscFieldIcon(field);
-
- const getTitle = (indexPatternField: IndexPatternField) => {
- return indexPatternField.displayName !== indexPatternField.name
- ? i18n.translate('discover.field.title', {
- defaultMessage: '{fieldName} ({fieldDisplayName})',
- values: {
- fieldName: indexPatternField.name,
- fieldDisplayName: indexPatternField.displayName,
- },
- })
- : indexPatternField.displayName;
- };
-
- const getFieldName = (indexPatternField: IndexPatternField) => {
- return (
-
- {wrapOnDot(indexPatternField.displayName)}
-
- );
- };
- const fieldName = getFieldName(field);
-
- const actionBtnClassName = classNames('dscSidebarItem__action', {
- ['dscSidebarItem__mobile']: alwaysShowActionButton,
- });
- const getActionButton = (f: IndexPatternField, isSelected?: boolean) => {
- if (f.name !== '_source' && !isSelected) {
- return (
-
- ) => {
- if (ev.type === 'click') {
- ev.currentTarget.focus();
- }
- ev.preventDefault();
- ev.stopPropagation();
- toggleDisplay(f, false);
- }}
- data-test-subj={`fieldToggle-${f.name}`}
- aria-label={addLabelAria}
- />
-
- );
- } else if (f.name !== '_source' && isSelected) {
- return (
-
- ) => {
- if (ev.type === 'click') {
- ev.currentTarget.focus();
- }
- ev.preventDefault();
- ev.stopPropagation();
- toggleDisplay(f, isSelected);
- }}
- data-test-subj={`fieldToggle-${f.name}`}
- aria-label={removeLabelAria}
- />
-
- );
- }
- };
-
- const actionButton = getActionButton(field, selected);
+ }, [infoIsOpen]);
if (field.type === '_source') {
return (
@@ -233,71 +288,20 @@ export function DiscoverField({
size="s"
className="dscSidebarItem"
dataTestSubj={`field-${field.name}-showDetails`}
- fieldIcon={dscFieldIcon}
- fieldAction={actionButton}
- fieldName={fieldName}
+ fieldIcon={ }
+ fieldAction={
+
+ }
+ fieldName={ }
/>
);
}
- const getFieldInfoIcon = () => {
- if (field.type !== 'conflict') {
- return null;
- }
- return (
-
-
-
- );
- };
-
- const fieldInfoIcon = getFieldInfoIcon();
-
- const shouldRenderMultiFields = !!multiFields;
- const renderMultiFields = () => {
- if (!multiFields) {
- return null;
- }
- return (
-
-
-
- {i18n.translate('discover.fieldChooser.discoverField.multiFields', {
- defaultMessage: 'Multi fields',
- })}
-
-
- {multiFields.map((entry) => (
- {}}
- dataTestSubj={`field-${entry.field.name}-showDetails`}
- fieldIcon={getDscFieldIcon(entry.field)}
- fieldAction={getActionButton(entry.field, entry.isSelected)}
- fieldName={getFieldName(entry.field)}
- key={entry.field.name}
- />
- ))}
-
- );
- };
-
const isRuntimeField = Boolean(indexPattern.getFieldByName(field.name)?.runtimeField);
const isUnknownField = field.type === 'unknown' || field.type === 'unknown_selected';
const canEditField = onEditField && (!isUnknownField || isRuntimeField);
@@ -334,9 +338,7 @@ export function DiscoverField({
>
{
- if (onDeleteField) {
- onDeleteField(field.name);
- }
+ onDeleteField?.(field.name);
}}
iconType="trash"
data-test-subj={`discoverFieldListPanelDelete-${field.name}`}
@@ -352,6 +354,8 @@ export function DiscoverField({
);
+ const details = getDetails(field);
+
return (
{
- togglePopover();
- }}
+ onClick={togglePopover}
dataTestSubj={`field-${field.name}-showDetails`}
- fieldIcon={dscFieldIcon}
- fieldAction={actionButton}
- fieldName={fieldName}
- fieldInfoIcon={fieldInfoIcon}
+ fieldIcon={ }
+ fieldAction={
+
+ }
+ fieldName={ }
+ fieldInfoIcon={field.type === 'conflict' && }
/>
}
isOpen={infoIsOpen}
@@ -384,26 +393,33 @@ export function DiscoverField({
{infoIsOpen && (
-
- )}
- {shouldRenderMultiFields ? (
<>
- {renderMultiFields()}
-
+ {multiFields && (
+
+ )}
+ {!details.error && (
+
+ )}
>
- ) : null}
+ )}
);
}
+
+export const DiscoverField = memo(DiscoverFieldComponent);
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx
index d7008ba3e310f..ffa7b30de5280 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx
@@ -20,7 +20,6 @@ import {
import { Bucket, FieldDetails } from './types';
import { IndexPatternField, IndexPattern } from '../../../../../../../data/public';
import './discover_field_details.scss';
-import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';
interface DiscoverFieldDetailsProps {
field: IndexPatternField;
@@ -28,7 +27,6 @@ interface DiscoverFieldDetailsProps {
details: FieldDetails;
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
- showFooter?: boolean;
}
export function DiscoverFieldDetails({
@@ -37,7 +35,6 @@ export function DiscoverFieldDetails({
details,
onAddFilter,
trackUiMetric,
- showFooter = true,
}: DiscoverFieldDetailsProps) {
const warnings = getWarnings(field);
const [showVisualizeLink, setShowVisualizeLink] = useState(false);
@@ -111,14 +108,6 @@ export function DiscoverFieldDetails({
>
)}
- {!details.error && showFooter && (
-
- )}
>
);
}
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 0bebec61657b4..7f8866a2ee369 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
@@ -21,6 +21,7 @@ import {
EuiPageSideBar,
useResizeObserver,
} from '@elastic/eui';
+import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
import { isEqual, sortBy } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -81,7 +82,6 @@ export function DiscoverSidebar({
trackUiMetric,
useNewFieldsApi = false,
useFlyout = false,
- unmappedFieldsConfig,
onEditRuntimeField,
onChangeIndexPattern,
setFieldEditorRef,
@@ -128,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(() => {
@@ -205,7 +188,7 @@ export function DiscoverSidebar({
return result;
}, [fields]);
- const multiFields = useMemo(() => {
+ const calculateMultiFields = () => {
if (!useNewFieldsApi || !fields) {
return undefined;
}
@@ -224,7 +207,13 @@ export function DiscoverSidebar({
map.set(parent, value);
});
return map;
- }, [fields, useNewFieldsApi, selectedFields]);
+ };
+
+ const [multiFields, setMultiFields] = useState(() => calculateMultiFields());
+
+ useShallowCompareEffect(() => {
+ setMultiFields(calculateMultiFields());
+ }, [fields, selectedFields, useNewFieldsApi]);
const deleteField = useMemo(
() =>
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 2ad75806173eb..6973221fd3624 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 cc33601f77728..003bb22599e48 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 5869720635621..cd9f6b3cac4a5 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 dc6cbcedc8086..2007d32fe84be 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 5cc7147b49ff9..07939fff6e7f4 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 051a2d2dcd9cc..4c3d819f063a0 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 a3546d54cd493..3c736f09a8296 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 5976c8fea5ea4..128c94f284f56 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 2b0d951724869..8c847b54078eb 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/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx
index 60841799b1398..50be2473a441e 100644
--- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx
+++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx
@@ -144,7 +144,9 @@ describe('Discover flyout', function () {
expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('4');
});
- it('allows navigating with arrow keys through documents', () => {
+ // EuiFlyout is mocked in Jest environments.
+ // EUI team to reinstate `onKeyDown`: https://github.com/elastic/eui/issues/4883
+ it.skip('allows navigating with arrow keys through documents', () => {
const props = getProps();
const component = mountWithIntl( );
findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' });
diff --git a/src/plugins/discover/public/application/components/doc/doc.tsx b/src/plugins/discover/public/application/components/doc/doc.tsx
index e38709b465174..ed8bcf30d2bd1 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 0000000000000..241e37c47a7e7
--- /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 f3a6b274649f5..9fdb564cb518d 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 7a3320d43c8b5..71a32b758aca7 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 8f07614813495..31dd6347218b5 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 5521df5b363ac..335805ed28493 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 b8427bb6bbdd2..f1ecd3ae3b70b 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 0000000000000..e5ab8bf4d1a0d
--- /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 0000000000000..68786871825ac
--- /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.
+
+
+
+
+
+
+
+
+
+
+
+
+ Refresh
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Source Viewer component renders json code editor 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Copy to clipboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+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 0000000000000..224e84ca50b52
--- /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 0000000000000..86433e5df6401
--- /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 0000000000000..94a12c04613a9
--- /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 fbe853ec6deb5..3840df4353faf 100644
--- a/src/plugins/discover/public/index.ts
+++ b/src/plugins/discover/public/index.ts
@@ -17,4 +17,6 @@ export function plugin(initializerContext: PluginInitializerContext) {
export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches';
export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable';
export { loadSharingDataHelpers } from './shared';
+
export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator';
+export { DiscoverAppLocator, DiscoverAppLocatorParams } from './locator';
diff --git a/src/plugins/discover/public/locator.test.ts b/src/plugins/discover/public/locator.test.ts
new file mode 100644
index 0000000000000..edbb0663d4aa3
--- /dev/null
+++ b/src/plugins/discover/public/locator.test.ts
@@ -0,0 +1,270 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public';
+import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock';
+import { FilterStateStore } from '../../data/common';
+import { DiscoverAppLocatorDefinition } from './locator';
+import { SerializableState } from 'src/plugins/kibana_utils/common';
+
+const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002';
+const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d';
+
+interface SetupParams {
+ useHash?: boolean;
+}
+
+const setup = async ({ useHash = false }: SetupParams = {}) => {
+ const locator = new DiscoverAppLocatorDefinition({
+ useHash,
+ });
+
+ return {
+ locator,
+ };
+};
+
+beforeEach(() => {
+ // @ts-expect-error
+ hashedItemStore.storage = mockStorage;
+});
+
+describe('Discover url generator', () => {
+ test('can create a link to Discover with no state and no saved search', async () => {
+ const { locator } = await setup();
+ const { app, path } = await locator.getLocation({});
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(app).toBe('discover');
+ expect(_a).toEqual({});
+ expect(_g).toEqual({});
+ });
+
+ test('can create a link to a saved search in Discover', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({ savedSearchId });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(path.startsWith(`#/view/${savedSearchId}`)).toBe(true);
+ expect(_a).toEqual({});
+ expect(_g).toEqual({});
+ });
+
+ test('can specify specific index pattern', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ indexPatternId,
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ index: indexPatternId,
+ });
+ expect(_g).toEqual({});
+ });
+
+ test('can specify specific time range', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ time: {
+ from: 'now-15m',
+ mode: 'relative',
+ to: 'now',
+ },
+ });
+ });
+
+ test('can specify query', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ query: {
+ language: 'kuery',
+ query: 'foo',
+ },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ query: {
+ language: 'kuery',
+ query: 'foo',
+ },
+ });
+ expect(_g).toEqual({});
+ });
+
+ test('can specify local and global filters', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ filters: [
+ {
+ meta: {
+ alias: 'foo',
+ disabled: false,
+ negate: false,
+ },
+ $state: {
+ store: FilterStateStore.APP_STATE,
+ },
+ },
+ {
+ meta: {
+ alias: 'bar',
+ disabled: false,
+ negate: false,
+ },
+ $state: {
+ store: FilterStateStore.GLOBAL_STATE,
+ },
+ },
+ ],
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ filters: [
+ {
+ $state: {
+ store: 'appState',
+ },
+ meta: {
+ alias: 'foo',
+ disabled: false,
+ negate: false,
+ },
+ },
+ ],
+ });
+ expect(_g).toEqual({
+ filters: [
+ {
+ $state: {
+ store: 'globalState',
+ },
+ meta: {
+ alias: 'bar',
+ disabled: false,
+ negate: false,
+ },
+ },
+ ],
+ });
+ });
+
+ test('can set refresh interval', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ refreshInterval: {
+ pause: false,
+ value: 666,
+ },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ refreshInterval: {
+ pause: false,
+ value: 666,
+ },
+ });
+ });
+
+ test('can set time range', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ timeRange: {
+ from: 'now-3h',
+ to: 'now',
+ },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ time: {
+ from: 'now-3h',
+ to: 'now',
+ },
+ });
+ });
+
+ test('can specify a search session id', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ searchSessionId: '__test__',
+ });
+
+ expect(path).toMatchInlineSnapshot(`"#/?_g=()&_a=()&searchSessionId=__test__"`);
+ expect(path).toContain('__test__');
+ });
+
+ test('can specify columns, interval, sort and savedQuery', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ columns: ['_source'],
+ interval: 'auto',
+ sort: [['timestamp, asc']] as string[][] & SerializableState,
+ savedQuery: '__savedQueryId__',
+ });
+
+ expect(path).toMatchInlineSnapshot(
+ `"#/?_g=()&_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"`
+ );
+ });
+
+ describe('useHash property', () => {
+ describe('when default useHash is set to false', () => {
+ test('when using default, sets index pattern ID in the generated URL', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(true);
+ });
+
+ test('when enabling useHash, does not set index pattern ID in the generated URL', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ useHash: true,
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(false);
+ });
+ });
+
+ describe('when default useHash is set to true', () => {
+ test('when using default, does not set index pattern ID in the generated URL', async () => {
+ const { locator } = await setup({ useHash: true });
+ const { path } = await locator.getLocation({
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(false);
+ });
+
+ test('when disabling useHash, sets index pattern ID in the generated URL', async () => {
+ const { locator } = await setup({ useHash: true });
+ const { path } = await locator.getLocation({
+ useHash: false,
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(true);
+ });
+ });
+ });
+});
diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts
new file mode 100644
index 0000000000000..fff89903bc465
--- /dev/null
+++ b/src/plugins/discover/public/locator.ts
@@ -0,0 +1,146 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { SerializableState } from 'src/plugins/kibana_utils/common';
+import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public';
+import type { LocatorDefinition, LocatorPublic } from '../../share/public';
+import { esFilters } from '../../data/public';
+import { setStateToKbnUrl } from '../../kibana_utils/public';
+
+export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR';
+
+export interface DiscoverAppLocatorParams extends SerializableState {
+ /**
+ * Optionally set saved search ID.
+ */
+ savedSearchId?: string;
+
+ /**
+ * Optionally set index pattern ID.
+ */
+ indexPatternId?: string;
+
+ /**
+ * Optionally set the time range in the time picker.
+ */
+ timeRange?: TimeRange;
+
+ /**
+ * Optionally set the refresh interval.
+ */
+ refreshInterval?: RefreshInterval & SerializableState;
+
+ /**
+ * Optionally apply filters.
+ */
+ filters?: Filter[];
+
+ /**
+ * Optionally set a query.
+ */
+ query?: Query;
+
+ /**
+ * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
+ * whether to hash the data in the url to avoid url length issues.
+ */
+ useHash?: boolean;
+
+ /**
+ * Background search session id
+ */
+ searchSessionId?: string;
+
+ /**
+ * Columns displayed in the table
+ */
+ columns?: string[];
+
+ /**
+ * Used interval of the histogram
+ */
+ interval?: string;
+
+ /**
+ * Array of the used sorting [[field,direction],...]
+ */
+ sort?: string[][] & SerializableState;
+
+ /**
+ * id of the used saved query
+ */
+ savedQuery?: string;
+}
+
+export type DiscoverAppLocator = LocatorPublic;
+
+export interface DiscoverAppLocatorDependencies {
+ useHash: boolean;
+}
+
+export class DiscoverAppLocatorDefinition implements LocatorDefinition {
+ public readonly id = DISCOVER_APP_LOCATOR;
+
+ constructor(protected readonly deps: DiscoverAppLocatorDependencies) {}
+
+ public readonly getLocation = async (params: DiscoverAppLocatorParams) => {
+ const {
+ useHash = this.deps.useHash,
+ filters,
+ indexPatternId,
+ query,
+ refreshInterval,
+ savedSearchId,
+ timeRange,
+ searchSessionId,
+ columns,
+ savedQuery,
+ sort,
+ interval,
+ } = params;
+ const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : '';
+ const appState: {
+ query?: Query;
+ filters?: Filter[];
+ index?: string;
+ columns?: string[];
+ interval?: string;
+ sort?: string[][];
+ savedQuery?: string;
+ } = {};
+ const queryState: QueryState = {};
+
+ if (query) appState.query = query;
+ if (filters && filters.length)
+ appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
+ if (indexPatternId) appState.index = indexPatternId;
+ if (columns) appState.columns = columns;
+ if (savedQuery) appState.savedQuery = savedQuery;
+ if (sort) appState.sort = sort;
+ if (interval) appState.interval = interval;
+
+ if (timeRange) queryState.time = timeRange;
+ if (filters && filters.length)
+ queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
+ if (refreshInterval) queryState.refreshInterval = refreshInterval;
+
+ let path = `#/${savedSearchPath}`;
+ path = setStateToKbnUrl('_g', queryState, { useHash }, path);
+ path = setStateToKbnUrl('_a', appState, { useHash }, path);
+
+ if (searchSessionId) {
+ path = `${path}&searchSessionId=${searchSessionId}`;
+ }
+
+ return {
+ app: 'discover',
+ path,
+ state: {},
+ };
+ };
+}
diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts
index 0f57c5c0fa138..53160df472a3c 100644
--- a/src/plugins/discover/public/mocks.ts
+++ b/src/plugins/discover/public/mocks.ts
@@ -16,6 +16,12 @@ const createSetupContract = (): Setup => {
docViews: {
addDocView: jest.fn(),
},
+ locator: {
+ getLocation: jest.fn(),
+ getUrl: jest.fn(),
+ useUrl: jest.fn(),
+ navigate: jest.fn(),
+ },
};
return setupContract;
};
@@ -26,6 +32,12 @@ const createStartContract = (): Start => {
urlGenerator: ({
createUrl: jest.fn(),
} as unknown) as DiscoverStart['urlGenerator'],
+ locator: {
+ getLocation: jest.fn(),
+ getUrl: jest.fn(),
+ useUrl: jest.fn(),
+ navigate: jest.fn(),
+ },
};
return startContract;
};
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index 139b23d28a1d4..ec89f7516e92d 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 0a27b4098681b..732aa35b05237 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx
@@ -13,7 +13,7 @@ import { Error } from '../types';
interface Props {
title: React.ReactNode;
- error: Error;
+ error?: Error;
actions?: JSX.Element;
isCentered?: boolean;
}
@@ -32,30 +32,30 @@ export const PageError: React.FunctionComponent = ({
isCentered,
...rest
}) => {
- const {
- error: errorString,
- cause, // wrapEsError() on the server adds a "cause" array
- message,
- } = error;
+ const errorString = error?.error;
+ const cause = error?.cause; // wrapEsError() on the server adds a "cause" array
+ const message = error?.message;
const errorContent = (
{title} }
body={
- <>
- {cause ? message || errorString : {message || errorString}
}
- {cause && (
- <>
-
-
- {cause.map((causeMsg, i) => (
- {causeMsg}
- ))}
-
- >
- )}
- >
+ error && (
+ <>
+ {cause ? message || errorString : {message || errorString}
}
+ {cause && (
+ <>
+
+
+ {cause.map((causeMsg, i) => (
+ {causeMsg}
+ ))}
+
+ >
+ )}
+ >
+ )
}
iconType="alert"
actions={actions}
diff --git a/packages/kbn-interpreter/src/common/lib/get_type.d.ts b/src/plugins/es_ui_shared/public/components/page_loading/index.ts
similarity index 87%
rename from packages/kbn-interpreter/src/common/lib/get_type.d.ts
rename to src/plugins/es_ui_shared/public/components/page_loading/index.ts
index 568658c780333..3e7b93bb4e7c3 100644
--- a/packages/kbn-interpreter/src/common/lib/get_type.d.ts
+++ b/src/plugins/es_ui_shared/public/components/page_loading/index.ts
@@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
-export declare function getType(node: any): string;
+export { PageLoading } from './page_loading';
diff --git a/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx
new file mode 100644
index 0000000000000..2fb99208e58ac
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText, EuiPageContent } from '@elastic/eui';
+
+export const PageLoading: React.FunctionComponent = ({ children }) => {
+ return (
+
+ }
+ body={{children} }
+ data-test-subj="sectionLoading"
+ />
+
+ );
+};
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
index 7b9013c043a0e..ef2e2daa25468 100644
--- a/src/plugins/es_ui_shared/public/index.ts
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -17,6 +17,7 @@ import * as XJson from './xjson';
export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './components/json_editor';
+export { PageLoading } from './components/page_loading';
export { SectionLoading } from './components/section_loading';
export { Frequency, CronEditor } from './components/cron_editor';
diff --git a/src/plugins/expressions/common/expression_functions/specs/math_column.ts b/src/plugins/expressions/common/expression_functions/specs/math_column.ts
index 0ff8faf3ce55a..633d912c29502 100644
--- a/src/plugins/expressions/common/expression_functions/specs/math_column.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/math_column.ts
@@ -69,25 +69,40 @@ export const mathColumn: ExpressionFunctionDefinition<
return id === args.id;
});
if (existingColumnIndex > -1) {
- throw new Error('ID must be unique');
+ throw new Error(
+ i18n.translate('expressions.functions.mathColumn.uniqueIdError', {
+ defaultMessage: 'ID must be unique',
+ })
+ );
}
const newRows = input.rows.map((row) => {
- return {
- ...row,
- [args.id]: math.fn(
- {
- type: 'datatable',
- columns: input.columns,
- rows: [row],
- },
- {
- expression: args.expression,
- onError: args.onError,
- },
- context
- ),
- };
+ const result = math.fn(
+ {
+ type: 'datatable',
+ columns: input.columns,
+ rows: [row],
+ },
+ {
+ expression: args.expression,
+ onError: args.onError,
+ },
+ context
+ );
+
+ if (Array.isArray(result)) {
+ if (result.length === 1) {
+ return { ...row, [args.id]: result[0] };
+ }
+ throw new Error(
+ i18n.translate('expressions.functions.mathColumn.arrayValueError', {
+ defaultMessage: 'Cannot perform math on array values at {name}',
+ values: { name: args.name },
+ })
+ );
+ }
+
+ return { ...row, [args.id]: result };
});
const type = newRows.length ? getType(newRows[0][args.id]) : 'null';
const newColumn: DatatableColumn = {
diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts
index bc6699a2b689b..e0fb0a3a9f23d 100644
--- a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts
@@ -34,6 +34,30 @@ describe('mathColumn', () => {
});
});
+ it('extracts a single array value, but not a multi-value array', () => {
+ const arrayTable = {
+ ...testTable,
+ rows: [
+ {
+ name: 'product1',
+ time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
+ price: [605, 500],
+ quantity: [100],
+ in_stock: true,
+ },
+ ],
+ };
+ const args = {
+ id: 'output',
+ name: 'output',
+ expression: 'quantity',
+ };
+ expect(fn(arrayTable, args).rows[0].output).toEqual(100);
+ expect(() => fn(arrayTable, { ...args, expression: 'price' })).toThrowError(
+ `Cannot perform math on array values`
+ );
+ });
+
it('handles onError', () => {
const args = {
id: 'output',
diff --git a/src/plugins/expressions/common/expression_types/get_type.test.ts b/src/plugins/expressions/common/expression_types/get_type.test.ts
index 6eca54d2aea44..b1a9cb703182f 100644
--- a/src/plugins/expressions/common/expression_types/get_type.test.ts
+++ b/src/plugins/expressions/common/expression_types/get_type.test.ts
@@ -30,6 +30,7 @@ describe('getType()', () => {
});
test('throws if object has no .type property', () => {
+ expect(() => getType([])).toThrow();
expect(() => getType({})).toThrow();
expect(() => getType({ _type: 'foo' })).toThrow();
expect(() => getType({ tipe: 'foo' })).toThrow();
diff --git a/src/plugins/expressions/common/expression_types/get_type.ts b/src/plugins/expressions/common/expression_types/get_type.ts
index e29a610b3ed90..052508df41329 100644
--- a/src/plugins/expressions/common/expression_types/get_type.ts
+++ b/src/plugins/expressions/common/expression_types/get_type.ts
@@ -8,6 +8,9 @@
export function getType(node: any) {
if (node == null) return 'null';
+ if (Array.isArray(node)) {
+ throw new Error('Unexpected array value encountered.');
+ }
if (typeof node === 'object') {
if (!node.type) throw new Error('Objects must have a type property');
return node.type;
diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js
index 1fda865ebd847..d7e6c07d6dd18 100644
--- a/src/plugins/home/public/application/components/tutorial_directory.js
+++ b/src/plugins/home/public/application/components/tutorial_directory.js
@@ -9,27 +9,15 @@
import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
+import { EuiFlexItem, EuiFlexGrid, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
+import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
import { Synopsis } from './synopsis';
import { SampleDataSetCards } from './sample_data_set_cards';
import { getServices } from '../kibana_services';
-
-import {
- EuiPage,
- EuiTabs,
- EuiTab,
- EuiFlexItem,
- EuiFlexGrid,
- EuiFlexGroup,
- EuiSpacer,
- EuiTitle,
- EuiPageBody,
-} from '@elastic/eui';
-
+import { KibanaPageTemplate } from '../../../../kibana_react/public';
import { getTutorials } from '../load_tutorials';
-import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-
const ALL_TAB_ID = 'all';
const SAMPLE_DATA_TAB_ID = 'sampleData';
@@ -184,17 +172,13 @@ class TutorialDirectoryUi extends React.Component {
});
};
- renderTabs = () => {
- return this.tabs.map((tab, index) => (
- this.onSelectedTabChanged(tab.id)}
- isSelected={tab.id === this.state.selectedTabId}
- key={index}
- >
- {tab.name}
-
- ));
+ getTabs = () => {
+ return this.tabs.map((tab) => ({
+ label: tab.name,
+ onClick: () => this.onSelectedTabChanged(tab.id),
+ isSelected: tab.id === this.state.selectedTabId,
+ 'data-test-subj': `homeTab-${tab.id}`,
+ }));
};
renderTabContent = () => {
@@ -258,41 +242,31 @@ class TutorialDirectoryUi extends React.Component {
) : null;
};
- renderHeader = () => {
- const notices = this.renderNotices();
+ render() {
const headerLinks = this.renderHeaderLinks();
+ const tabs = this.getTabs();
+ const notices = this.renderNotices();
return (
- <>
-
-
-
-
-
-
-
-
- {headerLinks ? {headerLinks} : null}
-
- {notices}
- >
- );
- };
-
- render() {
- return (
-
-
- {this.renderHeader()}
-
- {this.renderTabs()}
-
- {this.renderTabContent()}
-
-
+
+ ),
+ tabs,
+ rightSideItems: headerLinks ? [headerLinks] : [],
+ }}
+ >
+ {notices && (
+ <>
+ {notices}
+
+ >
+ )}
+ {this.renderTabContent()}
+
);
}
}
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx
index fc25879b128ec..77ef0903bc6fc 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx
@@ -216,7 +216,11 @@ const FieldEditorComponent = ({
Boolean(field?.type) && field?.type !== (updatedType && updatedType[0].value);
return (
-
}
+ />
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
index 67208c63ddf4c..e6d2c67d1baf8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
@@ -40,3 +40,19 @@ export interface RoleMapping {
const productNames = [APP_SEARCH_PLUGIN.NAME, WORKPLACE_SEARCH_PLUGIN.NAME] as const;
export type ProductName = typeof productNames[number];
+
+export interface Invitation {
+ email: string;
+ code: string;
+}
+
+export interface ElasticsearchUser {
+ email: string | null;
+ username: string;
+}
+
+export interface SingleUserRoleMapping {
+ invitation: Invitation;
+ elasticsearchUser: ElasticsearchUser;
+ roleMapping: T;
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx b/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx
index 6e89274dca570..a251188b5cd90 100644
--- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx
@@ -41,3 +41,9 @@ export const getPageHeaderActions = (wrapper: ShallowWrapper) => {
);
};
+
+export const getPageHeaderChildren = (wrapper: ShallowWrapper) => {
+ const children = getPageHeader(wrapper).children || null;
+
+ return shallow({children}
);
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts
index ed5c3f85a888e..7903b4a31c8a9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts
@@ -15,6 +15,7 @@ export {
getPageTitle,
getPageDescription,
getPageHeaderActions,
+ getPageHeaderChildren,
} from './get_page_header';
// Misc
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
index 3d5d0a8e6f2cf..f2601ff98db1d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
@@ -7,7 +7,10 @@
jest.mock('../../../shared/layout', () => ({
...jest.requireActual('../../../shared/layout'),
- generateNavLink: jest.fn(({ to }) => ({ href: to })),
+ generateNavLink: jest.fn(({ to, items }) => ({ href: to, items })),
+}));
+jest.mock('../../views/content_sources/components/source_sub_nav', () => ({
+ useSourceSubNav: () => [],
}));
jest.mock('../../views/groups/components/group_sub_nav', () => ({
useGroupSubNav: () => [],
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
index f59679e0ee048..ce2f8bf7ef7e4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
@@ -19,6 +19,7 @@ import {
GROUPS_PATH,
ORG_SETTINGS_PATH,
} from '../../routes';
+import { useSourceSubNav } from '../../views/content_sources/components/source_sub_nav';
import { useGroupSubNav } from '../../views/groups/components/group_sub_nav';
import { useSettingsSubNav } from '../../views/settings/components/settings_sub_nav';
@@ -32,8 +33,11 @@ export const useWorkplaceSearchNav = () => {
{
id: 'sources',
name: NAV.SOURCES,
- ...generateNavLink({ to: SOURCES_PATH }),
- items: [], // TODO: Source subnav
+ ...generateNavLink({
+ to: SOURCES_PATH,
+ shouldShowActiveForSubroutes: true,
+ items: useSourceSubNav(),
+ }),
},
{
id: 'groups',
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss
index 175f6b9ebca20..3287cb21783cb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss
@@ -6,18 +6,20 @@
*/
.personalDashboardLayout {
- $sideBarWidth: $euiSize * 30;
- $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes
- $pageHeight: calc(100vh - #{$consoleHeaderHeight});
+ &__sideBar {
+ padding: $euiSizeXL $euiSizeXXL $euiSizeXXL;
- left: $sideBarWidth;
- width: calc(100% - #{$sideBarWidth});
- min-height: $pageHeight;
+ @include euiBreakpoint('m', 'l') {
+ min-width: $euiSize * 20;
+ }
+ @include euiBreakpoint('xl') {
+ min-width: $euiSize * 30;
+ }
+ }
- &__sideBar {
- padding: 32px 40px 40px;
- width: $sideBarWidth;
- margin-left: -$sideBarWidth;
- height: $pageHeight;
+ &__body {
+ position: relative;
+ width: 100%;
+ height: 100%;
}
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx
index faeaa7323e93f..6847e91d46f6e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx
@@ -5,37 +5,102 @@
* 2.0.
*/
+import { setMockValues } from '../../../../__mocks__/kea_logic';
+import { mockUseRouteMatch } from '../../../../__mocks__/react_router';
+
import React from 'react';
import { shallow } from 'enzyme';
import { EuiCallOut } from '@elastic/eui';
-import { AccountHeader } from '..';
+import { FlashMessages } from '../../../../shared/flash_messages';
+import { SetWorkplaceSearchChrome } from '../../../../shared/kibana_chrome';
+import { Loading } from '../../../../shared/loading';
+
+import { AccountHeader, AccountSettingsSidebar, PrivateSourcesSidebar } from '../index';
import { PersonalDashboardLayout } from './personal_dashboard_layout';
describe('PersonalDashboardLayout', () => {
const children = test
;
- const sidebar = test
;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockValues({ readOnlyMode: false });
+ });
it('renders', () => {
- const wrapper = shallow(
- {children}
- );
+ const wrapper = shallow({children} );
expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1);
- expect(wrapper.find('[data-test-subj="TestSidebar"]')).toHaveLength(1);
+ expect(wrapper.find('.personalDashboardLayout')).toHaveLength(1);
expect(wrapper.find(AccountHeader)).toHaveLength(1);
+ expect(wrapper.find(FlashMessages)).toHaveLength(1);
});
- it('renders callout when in read-only mode', () => {
+ describe('renders sidebar content based on the route', () => {
+ it('renders the private sources sidebar on the private sources path', () => {
+ (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/p/sources');
+ const wrapper = shallow({children} );
+
+ expect(wrapper.find(PrivateSourcesSidebar)).toHaveLength(1);
+ });
+
+ it('renders the account settings sidebar on the account settings path', () => {
+ (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/p/settings');
+ const wrapper = shallow({children} );
+
+ expect(wrapper.find(AccountSettingsSidebar)).toHaveLength(1);
+ });
+
+ it('does not render a sidebar if not on a valid personal dashboard path', () => {
+ (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/test');
+ const wrapper = shallow({children} );
+
+ expect(wrapper.find(AccountSettingsSidebar)).toHaveLength(0);
+ expect(wrapper.find(PrivateSourcesSidebar)).toHaveLength(0);
+ });
+ });
+
+ describe('loading state', () => {
+ it('renders a loading icon in place of children', () => {
+ const wrapper = shallow(
+ {children}
+ );
+
+ expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(0);
+ });
+
+ it('renders children & does not render a loading icon when the page is done loading', () => {
+ const wrapper = shallow(
+ {children}
+ );
+
+ expect(wrapper.find(Loading)).toHaveLength(0);
+ expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1);
+ });
+ });
+
+ it('sets WS page chrome (primarily document title)', () => {
const wrapper = shallow(
-
+
{children}
);
+ expect(wrapper.find(SetWorkplaceSearchChrome).prop('trail')).toEqual([
+ 'Sources',
+ 'Add source',
+ 'Gmail',
+ ]);
+ });
+
+ it('renders callout when in read-only mode', () => {
+ setMockValues({ readOnlyMode: true });
+ const wrapper = shallow({children} );
+
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx
index 1ab9e07dfa14d..5b68d661ac5df 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx
@@ -6,44 +6,67 @@
*/
import React from 'react';
+import { useRouteMatch } from 'react-router-dom';
-import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui';
+import { useValues } from 'kea';
-import { AccountHeader } from '..';
+import {
+ EuiPage,
+ EuiPageSideBar,
+ EuiPageBody,
+ EuiPageContentBody,
+ EuiCallOut,
+ EuiSpacer,
+} from '@elastic/eui';
+import { FlashMessages } from '../../../../shared/flash_messages';
+import { HttpLogic } from '../../../../shared/http';
+import { SetWorkplaceSearchChrome } from '../../../../shared/kibana_chrome';
+import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
+import { Loading } from '../../../../shared/loading';
+
+import { PERSONAL_SOURCES_PATH, PERSONAL_SETTINGS_PATH } from '../../../routes';
import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING } from '../../../views/content_sources/constants';
+import { AccountHeader, AccountSettingsSidebar, PrivateSourcesSidebar } from '../index';
import './personal_dashboard_layout.scss';
interface LayoutProps {
- restrictWidth?: boolean;
- readOnlyMode?: boolean;
- sidebar: React.ReactNode;
+ isLoading?: boolean;
+ pageChrome?: BreadcrumbTrail;
}
export const PersonalDashboardLayout: React.FC = ({
children,
- restrictWidth,
- readOnlyMode,
- sidebar,
+ isLoading,
+ pageChrome,
}) => {
+ const { readOnlyMode } = useValues(HttpLogic);
+
return (
<>
+ {pageChrome && }
-
-
- {sidebar}
+
+
+ {useRouteMatch(PERSONAL_SOURCES_PATH) && }
+ {useRouteMatch(PERSONAL_SETTINGS_PATH) && }
-
- {readOnlyMode && (
-
- )}
- {children}
+
+
+ {readOnlyMode && (
+ <>
+
+
+ >
+ )}
+
+ {isLoading ? : children}
+
>
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
index 387724af970f8..9fa4d4dd1b237 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
@@ -7,17 +7,22 @@
import { setMockValues } from '../../../../__mocks__/kea_logic';
+jest.mock('../../../views/content_sources/components/source_sub_nav', () => ({
+ useSourceSubNav: () => [],
+}));
+
import React from 'react';
import { shallow } from 'enzyme';
+import { EuiSideNav } from '@elastic/eui';
+
import {
PRIVATE_CAN_CREATE_PAGE_TITLE,
PRIVATE_VIEW_ONLY_PAGE_TITLE,
PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION,
PRIVATE_CAN_CREATE_PAGE_DESCRIPTION,
} from '../../../constants';
-import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav';
import { ViewContentHeader } from '../../shared/view_content_header';
@@ -26,6 +31,7 @@ import { PrivateSourcesSidebar } from './private_sources_sidebar';
describe('PrivateSourcesSidebar', () => {
const mockValues = {
account: { canCreatePersonalSources: true },
+ contentSource: {},
};
beforeEach(() => {
@@ -36,25 +42,42 @@ describe('PrivateSourcesSidebar', () => {
const wrapper = shallow( );
expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
- expect(wrapper.find(SourceSubNav)).toHaveLength(1);
});
- it('uses correct title and description when private sources are enabled', () => {
- const wrapper = shallow( );
+ describe('header text', () => {
+ it('uses correct title and description when private sources are enabled', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE);
+ expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
+ PRIVATE_CAN_CREATE_PAGE_DESCRIPTION
+ );
+ });
- expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE);
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- PRIVATE_CAN_CREATE_PAGE_DESCRIPTION
- );
+ it('uses correct title and description when private sources are disabled', () => {
+ setMockValues({ ...mockValues, account: { canCreatePersonalSources: false } });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE);
+ expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
+ PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION
+ );
+ });
});
- it('uses correct title and description when private sources are disabled', () => {
- setMockValues({ account: { canCreatePersonalSources: false } });
- const wrapper = shallow( );
+ describe('sub nav', () => {
+ it('renders a side nav when viewing a single source', () => {
+ setMockValues({ ...mockValues, contentSource: { id: '1', name: 'test source' } });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiSideNav)).toHaveLength(1);
+ });
+
+ it('does not render a side nav if not on a source page', () => {
+ setMockValues({ ...mockValues, contentSource: {} });
+ const wrapper = shallow( );
- expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE);
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION
- );
+ expect(wrapper.find(EuiSideNav)).toHaveLength(0);
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
index 5505ae57b2ad5..3f6863175e29b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
@@ -9,6 +9,8 @@ import React from 'react';
import { useValues } from 'kea';
+import { EuiSideNav } from '@elastic/eui';
+
import { AppLogic } from '../../../app_logic';
import {
PRIVATE_CAN_CREATE_PAGE_TITLE,
@@ -16,7 +18,8 @@ import {
PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION,
PRIVATE_CAN_CREATE_PAGE_DESCRIPTION,
} from '../../../constants';
-import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav';
+import { useSourceSubNav } from '../../../views/content_sources/components/source_sub_nav';
+import { SourceLogic } from '../../../views/content_sources/source_logic';
import { ViewContentHeader } from '../../shared/view_content_header';
export const PrivateSourcesSidebar = () => {
@@ -31,10 +34,16 @@ export const PrivateSourcesSidebar = () => {
? PRIVATE_CAN_CREATE_PAGE_DESCRIPTION
: PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION;
+ const {
+ contentSource: { id = '', name = '' },
+ } = useValues(SourceLogic);
+
+ const navItems = [{ id, name, items: useSourceSubNav() }];
+
return (
<>
-
+ {id && }
>
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
index f4278d5083143..8a1e9c0275322 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
@@ -19,11 +19,6 @@ import { NotFound } from '../shared/not_found';
import { AppLogic } from './app_logic';
import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout';
-import {
- PersonalDashboardLayout,
- PrivateSourcesSidebar,
- AccountSettingsSidebar,
-} from './components/layout';
import {
GROUPS_PATH,
SETUP_GUIDE_PATH,
@@ -34,11 +29,11 @@ import {
ROLE_MAPPINGS_PATH,
SECURITY_PATH,
PERSONAL_SETTINGS_PATH,
+ PERSONAL_PATH,
} from './routes';
import { AccountSettings } from './views/account_settings';
import { SourcesRouter } from './views/content_sources';
import { SourceAdded } from './views/content_sources/components/source_added';
-import { SourceSubNav } from './views/content_sources/components/source_sub_nav';
import { ErrorState } from './views/error_state';
import { GroupsRouter } from './views/groups';
import { Overview } from './views/overview';
@@ -60,9 +55,6 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
const { pathname } = useLocation();
- // We don't want so show the subnavs on the container root pages.
- const showSourcesSubnav = pathname !== SOURCES_PATH && pathname !== PERSONAL_SOURCES_PATH;
-
/**
* Personal dashboard urls begin with /p/
* EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources
@@ -95,32 +87,18 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
-
- }
- >
-
-
-
-
- }
- >
-
-
+
+
+
+
+
+
+
+
+
- } />}
- restrictWidth
- readOnlyMode={readOnlyMode}
- >
-
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx
index a5a3d6b491bb9..b89a1451f7e57 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx
@@ -76,13 +76,13 @@ describe('getReindexJobRoute', () => {
it('should format org path', () => {
expect(getReindexJobRoute(SOURCE_ID, REINDEX_ID, true)).toEqual(
- `/sources/${SOURCE_ID}/schema_errors/${REINDEX_ID}`
+ `/sources/${SOURCE_ID}/schemas/${REINDEX_ID}`
);
});
it('should format user path', () => {
expect(getReindexJobRoute(SOURCE_ID, REINDEX_ID, false)).toEqual(
- `/p/sources/${SOURCE_ID}/schema_errors/${REINDEX_ID}`
+ `/p/sources/${SOURCE_ID}/schemas/${REINDEX_ID}`
);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts
index 1fe8019c4b364..3c564c1f912ec 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts
@@ -88,7 +88,7 @@ export const SOURCE_CONTENT_PATH = `${SOURCES_PATH}/:sourceId/content`;
export const SOURCE_SCHEMAS_PATH = `${SOURCES_PATH}/:sourceId/schemas`;
export const SOURCE_DISPLAY_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/display_settings`;
export const SOURCE_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/settings`;
-export const REINDEX_JOB_PATH = `${SOURCES_PATH}/:sourceId/schema_errors/:activeReindexJobId`;
+export const REINDEX_JOB_PATH = `${SOURCE_SCHEMAS_PATH}/:activeReindexJobId`;
export const DISPLAY_SETTINGS_SEARCH_RESULT_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/`;
export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/result_detail`;
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx
new file mode 100644
index 0000000000000..5ff80a7683db6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import '../../../__mocks__/shallow_useeffect.mock';
+import { mockKibanaValues } from '../../../__mocks__/kea_logic';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { AccountSettings } from './';
+
+describe('AccountSettings', () => {
+ const {
+ security: {
+ authc: { getCurrentUser },
+ uiApi: {
+ components: { getPersonalInfo, getChangePassword },
+ },
+ },
+ } = mockKibanaValues;
+
+ const mockCurrentUser = (user?: unknown) =>
+ (getCurrentUser as jest.Mock).mockReturnValue(Promise.resolve(user));
+
+ beforeAll(() => {
+ mockCurrentUser();
+ });
+
+ it('gets the current user on mount', () => {
+ shallow( );
+
+ expect(getCurrentUser).toHaveBeenCalled();
+ });
+
+ it('does not render if the current user does not exist', async () => {
+ mockCurrentUser(null);
+ const wrapper = await shallow( );
+
+ expect(wrapper.isEmptyRender()).toBe(true);
+ });
+
+ it('renders the security UI components when the user exists', async () => {
+ mockCurrentUser({ username: 'mock user' });
+ (getPersonalInfo as jest.Mock).mockReturnValue(
);
+ (getChangePassword as jest.Mock).mockReturnValue(
);
+
+ const wrapper = await shallow( );
+
+ expect(wrapper.childAt(0).dive().find('[data-test-subj="PersonalInfo"]')).toHaveLength(1);
+ expect(wrapper.childAt(1).dive().find('[data-test-subj="ChangePassword"]')).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx
index e28faaeec8993..313d3ffa59d48 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx
@@ -11,6 +11,8 @@ import { useValues } from 'kea';
import type { AuthenticatedUser } from '../../../../../../security/public';
import { KibanaLogic } from '../../../shared/kibana/kibana_logic';
+import { PersonalDashboardLayout } from '../../components/layout';
+import { ACCOUNT_SETTINGS_TITLE } from '../../constants';
export const AccountSettings: React.FC = () => {
const { security } = useValues(KibanaLogic);
@@ -31,9 +33,9 @@ export const AccountSettings: React.FC = () => {
}
return (
- <>
+
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx
index 92cbfcf6eeafe..0501509b3a8ef 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx
@@ -17,7 +17,10 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { Loading } from '../../../../../shared/loading';
+import {
+ WorkplaceSearchPageTemplate,
+ PersonalDashboardLayout,
+} from '../../../../components/layout';
import { AddSource } from './add_source';
import { AddSourceSteps } from './add_source_logic';
@@ -68,11 +71,27 @@ describe('AddSourceList', () => {
expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep);
});
- it('handles loading state', () => {
- setMockValues({ ...mockValues, dataLoading: true });
+ describe('layout', () => {
+ it('renders the default workplace search layout when on an organization view', () => {
+ setMockValues({ ...mockValues, isOrganization: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
+ });
+
+ it('renders the personal dashboard layout when not in an organization', () => {
+ setMockValues({ ...mockValues, isOrganization: false });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(PersonalDashboardLayout);
+ });
+ });
+
+ it('renders a breadcrumb fallback while data is loading', () => {
+ setMockValues({ ...mockValues, dataLoading: true, sourceConfigData: {} });
const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.prop('pageChrome')).toEqual(['Sources', 'Add Source', '...']);
});
it('renders Config Completed step', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx
index ee4bcfb9afd34..b0c3ebe64830c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx
@@ -13,9 +13,12 @@ import { i18n } from '@kbn/i18n';
import { setSuccessMessage } from '../../../../../shared/flash_messages';
import { KibanaLogic } from '../../../../../shared/kibana';
-import { Loading } from '../../../../../shared/loading';
import { AppLogic } from '../../../../app_logic';
-import { CUSTOM_SERVICE_TYPE } from '../../../../constants';
+import {
+ WorkplaceSearchPageTemplate,
+ PersonalDashboardLayout,
+} from '../../../../components/layout';
+import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants';
import { SOURCES_PATH, getSourcesPath } from '../../../../routes';
import { SourceDataItem } from '../../../../types';
import { staticSourceData } from '../../source_data';
@@ -71,8 +74,6 @@ export const AddSource: React.FC = (props) => {
return resetSourceState;
}, []);
- if (dataLoading) return ;
-
const goToConfigurationIntro = () => setAddSourceStep(AddSourceSteps.ConfigIntroStep);
const goToSaveConfig = () => setAddSourceStep(AddSourceSteps.SaveConfigStep);
const setConfigCompletedStep = () => setAddSourceStep(AddSourceSteps.ConfigCompletedStep);
@@ -99,9 +100,10 @@ export const AddSource: React.FC = (props) => {
};
const header = ;
+ const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
return (
- <>
+
{addSourceCurrentStep === AddSourceSteps.ConfigIntroStep && (
)}
@@ -158,6 +160,6 @@ export const AddSource: React.FC = (props) => {
{addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && (
)}
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx
index 6bf71cd73ec35..b30511f0a6d80 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx
@@ -19,7 +19,11 @@ import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui';
-import { Loading } from '../../../../../shared/loading';
+import { getPageDescription } from '../../../../../test_helpers';
+import {
+ WorkplaceSearchPageTemplate,
+ PersonalDashboardLayout,
+} from '../../../../components/layout';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
import { AddSourceList } from './add_source_list';
@@ -54,14 +58,21 @@ describe('AddSourceList', () => {
expect(wrapper.find(AvailableSourcesList)).toHaveLength(1);
});
- it('returns loading when loading', () => {
- setMockValues({
- ...mockValues,
- dataLoading: true,
+ describe('layout', () => {
+ it('renders the default workplace search layout when on an organization view', () => {
+ setMockValues({ ...mockValues, isOrganization: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
});
- const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ it('renders the personal dashboard layout and a header when not in an organization', () => {
+ setMockValues({ ...mockValues, isOrganization: false });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(PersonalDashboardLayout);
+ expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
+ });
});
describe('filters sources', () => {
@@ -97,49 +108,51 @@ describe('AddSourceList', () => {
});
describe('content headings', () => {
- it('should render correct organization heading with sources', () => {
- const wrapper = shallow( );
-
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- ADD_SOURCE_ORG_SOURCE_DESCRIPTION
- );
- });
+ describe('organization view', () => {
+ it('should render the correct organization heading with sources', () => {
+ const wrapper = shallow( );
- it('should render correct organization heading without sources', () => {
- setMockValues({
- ...mockValues,
- contentSources: [],
+ expect(getPageDescription(wrapper)).toEqual(ADD_SOURCE_ORG_SOURCE_DESCRIPTION);
});
- const wrapper = shallow( );
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_ORG_SOURCE_DESCRIPTION
- );
- });
+ it('should render the correct organization heading without sources', () => {
+ setMockValues({
+ ...mockValues,
+ contentSources: [],
+ });
+ const wrapper = shallow( );
- it('should render correct account heading with sources', () => {
- const wrapper = shallow( );
- setMockValues({
- ...mockValues,
- isOrganization: false,
+ expect(getPageDescription(wrapper)).toEqual(
+ ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_ORG_SOURCE_DESCRIPTION
+ );
});
-
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- ADD_SOURCE_ORG_SOURCE_DESCRIPTION
- );
});
- it('should render correct account heading without sources', () => {
- setMockValues({
- ...mockValues,
- isOrganization: false,
- contentSources: [],
+ describe('personal dashboard view', () => {
+ it('should render the correct personal heading with sources', () => {
+ setMockValues({
+ ...mockValues,
+ isOrganization: false,
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
+ ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION
+ );
});
- const wrapper = shallow( );
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION
- );
+ it('should render the correct personal heading without sources', () => {
+ setMockValues({
+ ...mockValues,
+ isOrganization: false,
+ contentSources: [],
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
+ ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION
+ );
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx
index 80d35553bb8bb..a7a64194cb42f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx
@@ -19,12 +19,15 @@ import {
EuiEmptyPrompt,
} from '@elastic/eui';
-import { Loading } from '../../../../../shared/loading';
import { AppLogic } from '../../../../app_logic';
import noSharedSourcesIcon from '../../../../assets/share_circle.svg';
+import {
+ WorkplaceSearchPageTemplate,
+ PersonalDashboardLayout,
+} from '../../../../components/layout';
import { ContentSection } from '../../../../components/shared/content_section';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
-import { CUSTOM_SERVICE_TYPE } from '../../../../constants';
+import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants';
import { SourceDataItem } from '../../../../types';
import { SourcesLogic } from '../../sources_logic';
@@ -58,8 +61,6 @@ export const AddSourceList: React.FC = () => {
return resetSourcesState;
}, []);
- if (dataLoading) return ;
-
const hasSources = contentSources.length > 0;
const showConfiguredSourcesList = configuredSources.find(
({ serviceType }) => serviceType !== CUSTOM_SERVICE_TYPE
@@ -97,12 +98,22 @@ export const AddSourceList: React.FC = () => {
filterConfiguredSources
) as SourceDataItem[];
+ const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
+
return (
- <>
-
+
+ {!isOrganization && (
+
+
+
+ )}
{showConfiguredSourcesList || isOrganization ? (
-
{
)}
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx
index aa5cec385738d..e5714bf4bdfbf 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx
@@ -16,7 +16,6 @@ import { shallow } from 'enzyme';
import { EuiButton, EuiTabbedContent } from '@elastic/eui';
-import { Loading } from '../../../../../shared/loading';
import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
@@ -57,13 +56,6 @@ describe('DisplaySettings', () => {
expect(wrapper.find('form')).toHaveLength(1);
});
- it('returns loading when loading', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
describe('tabbed content', () => {
const tabs = [
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx
index d923fbe7a1a8e..ae47e20026b68 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx
@@ -20,10 +20,10 @@ import {
} from '@elastic/eui';
import { clearFlashMessages } from '../../../../../shared/flash_messages';
-import { Loading } from '../../../../../shared/loading';
import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
-import { SAVE_BUTTON } from '../../../../constants';
+import { NAV, SAVE_BUTTON } from '../../../../constants';
+import { SourceLayout } from '../source_layout';
import {
UNSAVED_MESSAGE,
@@ -64,8 +64,6 @@ export const DisplaySettings: React.FC = ({ tabId }) => {
return clearFlashMessages;
}, []);
- if (dataLoading) return ;
-
const tabs = [
{
id: 'search_results',
@@ -89,7 +87,11 @@ export const DisplaySettings: React.FC = ({ tabId }) => {
};
return (
- <>
+
= ({ tabId }) => {
)}
{addFieldModalVisible && }
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts
index e8b419a31abb2..38424df724bd4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts
@@ -5,11 +5,11 @@
* 2.0.
*/
-import { DropResult } from 'react-beautiful-dnd';
-
import { kea, MakeLogicType } from 'kea';
import { cloneDeep, isEqual, differenceBy } from 'lodash';
+import { DropResult } from '@elastic/eui';
+
import {
setSuccessMessage,
clearFlashMessages,
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx
index f2cf5f50b813b..d99eac5de74e5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-import '../../../../__mocks__/shallow_useeffect.mock';
-
import { setMockValues } from '../../../../__mocks__/kea_logic';
import { fullContentSources } from '../../../__mocks__/content_sources.mock';
@@ -16,7 +14,6 @@ import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiPanel, EuiTable } from '@elastic/eui';
-import { Loading } from '../../../../shared/loading';
import { ComponentLoader } from '../../../components/shared/component_loader';
import { Overview } from './overview';
@@ -44,13 +41,6 @@ describe('Overview', () => {
expect(documentSummary.find('[data-test-subj="DocumentSummaryRow"]')).toHaveLength(1);
});
- it('returns Loading when loading', () => {
- setMockValues({ ...mockValues, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('renders ComponentLoader when loading', () => {
setMockValues({
...mockValues,
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx
index 153df1bc00496..cc890e0f104ac 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx
@@ -29,7 +29,6 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { Loading } from '../../../../shared/loading';
import { EuiPanelTo } from '../../../../shared/react_router_helpers';
import { AppLogic } from '../../../app_logic';
import aclImage from '../../../assets/supports_acl.svg';
@@ -78,8 +77,10 @@ import {
} from '../constants';
import { SourceLogic } from '../source_logic';
+import { SourceLayout } from './source_layout';
+
export const Overview: React.FC = () => {
- const { contentSource, dataLoading } = useValues(SourceLogic);
+ const { contentSource } = useValues(SourceLogic);
const { isOrganization } = useValues(AppLogic);
const {
@@ -97,8 +98,6 @@ export const Overview: React.FC = () => {
isFederatedSource,
} = contentSource;
- if (dataLoading) return ;
-
const DocumentSummary = () => {
let totalDocuments = 0;
const tableContent = summary?.map((item, index) => {
@@ -450,8 +449,9 @@ export const Overview: React.FC = () => {
);
return (
- <>
+
+
@@ -513,6 +513,6 @@ export const Overview: React.FC = () => {
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx
index 178c9125ee437..47859e4e67b17 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx
@@ -16,7 +16,6 @@ import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui';
-import { Loading } from '../../../../../shared/loading';
import { SchemaAddFieldModal, SchemaErrorsCallout } from '../../../../../shared/schema';
import { Schema } from './schema';
@@ -71,13 +70,6 @@ describe('Schema', () => {
expect(wrapper.find(SchemaFieldsTable)).toHaveLength(1);
});
- it('returns loading when loading', () => {
- setMockValues({ ...mockValues, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('handles empty state', () => {
setMockValues({ ...mockValues, activeSchema: {} });
const wrapper = shallow( );
@@ -106,7 +98,7 @@ describe('Schema', () => {
expect(wrapper.find(SchemaErrorsCallout)).toHaveLength(1);
expect(wrapper.find(SchemaErrorsCallout).prop('viewErrorsPath')).toEqual(
- '/sources/123/schema_errors/123'
+ '/sources/123/schemas/123'
);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx
index 65ed988f45ff0..a0efebdcb5a48 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx
@@ -20,11 +20,12 @@ import {
EuiPanel,
} from '@elastic/eui';
-import { Loading } from '../../../../../shared/loading';
import { SchemaAddFieldModal, SchemaErrorsCallout } from '../../../../../shared/schema';
import { AppLogic } from '../../../../app_logic';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
+import { NAV } from '../../../../constants';
import { getReindexJobRoute } from '../../../../routes';
+import { SourceLayout } from '../source_layout';
import {
SCHEMA_ADD_FIELD_BUTTON,
@@ -65,8 +66,6 @@ export const Schema: React.FC = () => {
initializeSchema();
}, []);
- if (dataLoading) return ;
-
const hasSchemaFields = Object.keys(activeSchema).length > 0;
const { hasErrors, activeReindexJobId } = mostRecentIndexJob;
@@ -77,7 +76,11 @@ export const Schema: React.FC = () => {
);
return (
- <>
+
{
closeAddFieldModal={closeAddFieldModal}
/>
)}
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx
index e300823aa3ed3..eb07beda73327 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx
@@ -12,6 +12,8 @@ import { useActions, useValues } from 'kea';
import { SchemaErrorsAccordion } from '../../../../../shared/schema';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
+import { NAV } from '../../../../constants';
+import { SourceLayout } from '../source_layout';
import { SCHEMA_ERRORS_HEADING } from './constants';
import { SchemaLogic } from './schema_logic';
@@ -30,9 +32,12 @@ export const SchemaChangeErrors: React.FC = () => {
}, []);
return (
- <>
+
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx
index 4bcc4b16166d1..9304f0f344a1b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx
@@ -25,7 +25,6 @@ import {
} from '@elastic/eui';
import { DEFAULT_META } from '../../../../shared/constants';
-import { Loading } from '../../../../shared/loading';
import { ComponentLoader } from '../../../components/shared/component_loader';
import { TablePaginationBar } from '../../../components/shared/table_pagination_bar';
@@ -61,13 +60,6 @@ describe('SourceContent', () => {
expect(wrapper.find(EuiTable)).toHaveLength(1);
});
- it('returns Loading when loading', () => {
- setMockValues({ ...mockValues, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('returns ComponentLoader when section loading', () => {
setMockValues({ ...mockValues, sectionLoading: true });
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
index fbafe54df7493..a0e3c28f20eb0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
@@ -31,12 +31,11 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { Loading } from '../../../../shared/loading';
import { TruncatedContent } from '../../../../shared/truncate';
import { ComponentLoader } from '../../../components/shared/component_loader';
import { TablePaginationBar } from '../../../components/shared/table_pagination_bar';
import { ViewContentHeader } from '../../../components/shared/view_content_header';
-import { CUSTOM_SERVICE_TYPE } from '../../../constants';
+import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants';
import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes';
import { SourceContentItem } from '../../../types';
import {
@@ -51,6 +50,8 @@ import {
} from '../constants';
import { SourceLogic } from '../source_logic';
+import { SourceLayout } from './source_layout';
+
const MAX_LENGTH = 28;
export const SourceContent: React.FC = () => {
@@ -67,7 +68,6 @@ export const SourceContent: React.FC = () => {
},
contentItems,
contentFilterValue,
- dataLoading,
sectionLoading,
} = useValues(SourceLogic);
@@ -75,8 +75,6 @@ export const SourceContent: React.FC = () => {
searchContentSourceDocuments(id);
}, [contentFilterValue, activePage]);
- if (dataLoading) return ;
-
const showPagination = totalPages > 1;
const hasItems = totalItems > 0;
const emptyMessage = contentFilterValue
@@ -193,7 +191,7 @@ export const SourceContent: React.FC = () => {
);
return (
- <>
+
@@ -219,6 +217,6 @@ export const SourceContent: React.FC = () => {
{sectionLoading && }
{!sectionLoading && (hasItems ? contentTable : emptyState)}
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx
new file mode 100644
index 0000000000000..7c7d77ec418e7
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import '../../../../__mocks__/shallow_useeffect.mock';
+
+import { setMockValues } from '../../../../__mocks__/kea_logic';
+import { contentSources } from '../../../__mocks__/content_sources.mock';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiCallOut } from '@elastic/eui';
+
+import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout';
+
+import { SourceInfoCard } from './source_info_card';
+import { SourceLayout } from './source_layout';
+
+describe('SourceLayout', () => {
+ const contentSource = contentSources[1];
+ const mockValues = {
+ contentSource,
+ dataLoading: false,
+ isOrganization: true,
+ };
+
+ beforeEach(() => {
+ setMockValues({ ...mockValues });
+ });
+
+ it('renders', () => {
+ const wrapper = shallow(
+
+
+
+ );
+
+ expect(wrapper.find(SourceInfoCard)).toHaveLength(1);
+ expect(wrapper.find('.testChild')).toHaveLength(1);
+ });
+
+ it('renders the default Workplace Search layout when on an organization view', () => {
+ setMockValues({ ...mockValues, isOrganization: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
+ });
+
+ it('renders a personal dashboard layout when not on an organization view', () => {
+ setMockValues({ ...mockValues, isOrganization: false });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(PersonalDashboardLayout);
+ });
+
+ it('passes any page template props to the underlying page template', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(WorkplaceSearchPageTemplate).prop('pageViewTelemetry')).toEqual('test');
+ });
+
+ it('handles breadcrumbs while loading', () => {
+ setMockValues({
+ ...mockValues,
+ contentSource: {},
+ dataLoading: true,
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('pageChrome')).toEqual(['Sources', '...']);
+ });
+
+ it('renders a callout when the source is not supported by the current license', () => {
+ setMockValues({ ...mockValues, contentSource: { supportedByLicense: false } });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiCallOut)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx
new file mode 100644
index 0000000000000..446e93e0c61f3
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright 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 { useValues } from 'kea';
+import moment from 'moment';
+
+import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
+
+import { PageTemplateProps } from '../../../../shared/layout';
+import { AppLogic } from '../../../app_logic';
+import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout';
+import { NAV } from '../../../constants';
+import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes';
+
+import {
+ SOURCE_DISABLED_CALLOUT_TITLE,
+ SOURCE_DISABLED_CALLOUT_DESCRIPTION,
+ SOURCE_DISABLED_CALLOUT_BUTTON,
+} from '../constants';
+import { SourceLogic } from '../source_logic';
+
+import { SourceInfoCard } from './source_info_card';
+
+export const SourceLayout: React.FC = ({
+ children,
+ pageChrome = [],
+ ...props
+}) => {
+ const { contentSource, dataLoading } = useValues(SourceLogic);
+ const { isOrganization } = useValues(AppLogic);
+
+ const {
+ name,
+ createdAt,
+ serviceType,
+ serviceName,
+ isFederatedSource,
+ supportedByLicense,
+ } = contentSource;
+
+ const pageHeader = (
+ <>
+
+
+ >
+ );
+
+ const callout = (
+ <>
+
+ {SOURCE_DISABLED_CALLOUT_DESCRIPTION}
+
+ {SOURCE_DISABLED_CALLOUT_BUTTON}
+
+
+
+ >
+ );
+
+ const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
+
+ return (
+
+ {!supportedByLicense && callout}
+ {pageHeader}
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
index aa6cbf3cf6574..667e7fd4dbfb4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
@@ -26,6 +26,8 @@ import { AppLogic } from '../../../app_logic';
import { ContentSection } from '../../../components/shared/content_section';
import { SourceConfigFields } from '../../../components/shared/source_config_fields';
import { ViewContentHeader } from '../../../components/shared/view_content_header';
+import { NAV } from '../../../constants';
+
import {
CANCEL_BUTTON,
OK_BUTTON,
@@ -52,6 +54,8 @@ import {
import { staticSourceData } from '../source_data';
import { SourceLogic } from '../source_logic';
+import { SourceLayout } from './source_layout';
+
export const SourceSettings: React.FC = () => {
const { updateContentSource, removeContentSource } = useActions(SourceLogic);
const { getSourceConfigData } = useActions(AddSourceLogic);
@@ -128,7 +132,7 @@ export const SourceSettings: React.FC = () => {
);
return (
- <>
+
@@ -197,6 +201,6 @@ export const SourceSettings: React.FC = () => {
{confirmModalVisible && confirmModal}
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx
index 25c389419d731..7f07c59587f96 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx
@@ -7,34 +7,92 @@
import { setMockValues } from '../../../../__mocks__/kea_logic';
-import React from 'react';
+jest.mock('../../../../shared/layout', () => ({
+ generateNavLink: jest.fn(({ to }) => ({ href: to })),
+}));
-import { shallow } from 'enzyme';
+import { useSourceSubNav } from './source_sub_nav';
-import { SideNavLink } from '../../../../shared/layout';
-import { CUSTOM_SERVICE_TYPE } from '../../../constants';
+describe('useSourceSubNav', () => {
+ it('returns undefined when no content source id present', () => {
+ setMockValues({ contentSource: {} });
-import { SourceSubNav } from './source_sub_nav';
+ expect(useSourceSubNav()).toEqual(undefined);
+ });
-describe('SourceSubNav', () => {
- it('renders empty when no group id present', () => {
- setMockValues({ contentSource: {} });
- const wrapper = shallow( );
+ it('returns EUI nav items', () => {
+ setMockValues({ isOrganization: true, contentSource: { id: '1' } });
- expect(wrapper.find(SideNavLink)).toHaveLength(0);
+ expect(useSourceSubNav()).toEqual([
+ {
+ id: 'sourceOverview',
+ name: 'Overview',
+ href: '/sources/1',
+ },
+ {
+ id: 'sourceContent',
+ name: 'Content',
+ href: '/sources/1/content',
+ },
+ {
+ id: 'sourceSettings',
+ name: 'Settings',
+ href: '/sources/1/settings',
+ },
+ ]);
});
- it('renders nav items', () => {
- setMockValues({ contentSource: { id: '1' } });
- const wrapper = shallow( );
+ it('returns extra nav items for custom sources', () => {
+ setMockValues({ isOrganization: true, contentSource: { id: '2', serviceType: 'custom' } });
- expect(wrapper.find(SideNavLink)).toHaveLength(3);
+ expect(useSourceSubNav()).toEqual([
+ {
+ id: 'sourceOverview',
+ name: 'Overview',
+ href: '/sources/2',
+ },
+ {
+ id: 'sourceContent',
+ name: 'Content',
+ href: '/sources/2/content',
+ },
+ {
+ id: 'sourceSchema',
+ name: 'Schema',
+ href: '/sources/2/schemas',
+ },
+ {
+ id: 'sourceDisplaySettings',
+ name: 'Display Settings',
+ href: '/sources/2/display_settings',
+ },
+ {
+ id: 'sourceSettings',
+ name: 'Settings',
+ href: '/sources/2/settings',
+ },
+ ]);
});
- it('renders custom source nav items', () => {
- setMockValues({ contentSource: { id: '1', serviceType: CUSTOM_SERVICE_TYPE } });
- const wrapper = shallow( );
+ it('returns nav links to personal dashboard when not on an organization page', () => {
+ setMockValues({ isOrganization: false, contentSource: { id: '3' } });
- expect(wrapper.find(SideNavLink)).toHaveLength(5);
+ expect(useSourceSubNav()).toEqual([
+ {
+ id: 'sourceOverview',
+ name: 'Overview',
+ href: '/p/sources/3',
+ },
+ {
+ id: 'sourceContent',
+ name: 'Content',
+ href: '/p/sources/3/content',
+ },
+ {
+ id: 'sourceSettings',
+ name: 'Settings',
+ href: '/p/sources/3/settings',
+ },
+ ]);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx
index 12e1506ec6efd..6b595a06f0404 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx
@@ -5,11 +5,11 @@
* 2.0.
*/
-import React from 'react';
-
import { useValues } from 'kea';
-import { SideNavLink } from '../../../../shared/layout';
+import { EuiSideNavItemType } from '@elastic/eui';
+
+import { generateNavLink } from '../../../../shared/layout';
import { AppLogic } from '../../../app_logic';
import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants';
import {
@@ -22,40 +22,52 @@ import {
} from '../../../routes';
import { SourceLogic } from '../source_logic';
-export const SourceSubNav: React.FC = () => {
+export const useSourceSubNav = () => {
const { isOrganization } = useValues(AppLogic);
const {
contentSource: { id, serviceType },
} = useValues(SourceLogic);
- if (!id) return null;
+ if (!id) return undefined;
+
+ const navItems: Array> = [
+ {
+ id: 'sourceOverview',
+ name: NAV.OVERVIEW,
+ ...generateNavLink({ to: getContentSourcePath(SOURCE_DETAILS_PATH, id, isOrganization) }),
+ },
+ {
+ id: 'sourceContent',
+ name: NAV.CONTENT,
+ ...generateNavLink({ to: getContentSourcePath(SOURCE_CONTENT_PATH, id, isOrganization) }),
+ },
+ ];
const isCustom = serviceType === CUSTOM_SERVICE_TYPE;
+ if (isCustom) {
+ navItems.push({
+ id: 'sourceSchema',
+ name: NAV.SCHEMA,
+ ...generateNavLink({
+ to: getContentSourcePath(SOURCE_SCHEMAS_PATH, id, isOrganization),
+ shouldShowActiveForSubroutes: true,
+ }),
+ });
+ navItems.push({
+ id: 'sourceDisplaySettings',
+ name: NAV.DISPLAY_SETTINGS,
+ ...generateNavLink({
+ to: getContentSourcePath(SOURCE_DISPLAY_SETTINGS_PATH, id, isOrganization),
+ shouldShowActiveForSubroutes: true,
+ }),
+ });
+ }
+
+ navItems.push({
+ id: 'sourceSettings',
+ name: NAV.SETTINGS,
+ ...generateNavLink({ to: getContentSourcePath(SOURCE_SETTINGS_PATH, id, isOrganization) }),
+ });
- return (
-
-
- {NAV.OVERVIEW}
-
-
- {NAV.CONTENT}
-
- {isCustom && (
- <>
-
- {NAV.SCHEMA}
-
-
- {NAV.DISPLAY_SETTINGS}
-
- >
- )}
-
- {NAV.SETTINGS}
-
-
- );
+ return navItems;
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx
index 9df91406c4b7b..2317c84af2432 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx
@@ -10,14 +10,10 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
import { contentSources } from '../../__mocks__/content_sources.mock';
import React from 'react';
-import { Redirect } from 'react-router-dom';
import { shallow } from 'enzyme';
-import { Loading } from '../../../shared/loading';
import { SourcesTable } from '../../components/shared/sources_table';
-import { ViewContentHeader } from '../../components/shared/view_content_header';
-import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes';
import { OrganizationSources } from './organization_sources';
@@ -42,20 +38,12 @@ describe('OrganizationSources', () => {
const wrapper = shallow( );
expect(wrapper.find(SourcesTable)).toHaveLength(1);
- expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
});
- it('returns loading when loading', () => {
+ it('does not render a page header when data is loading (to prevent a jump after redirect)', () => {
setMockValues({ ...mockValues, dataLoading: true });
const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
- it('returns redirect when no sources', () => {
- setMockValues({ ...mockValues, contentSources: [] });
- const wrapper = shallow( );
-
- expect(wrapper.find(Redirect).prop('to')).toEqual(getSourcesPath(ADD_SOURCE_PATH, true));
+ expect(wrapper.prop('pageHeader')).toBeUndefined();
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx
index 4559003b4597f..a4273ae2ae6a2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx
@@ -6,16 +6,15 @@
*/
import React, { useEffect } from 'react';
-import { Link, Redirect } from 'react-router-dom';
+import { Redirect } from 'react-router-dom';
import { useActions, useValues } from 'kea';
-import { EuiButton } from '@elastic/eui';
-
-import { Loading } from '../../../shared/loading';
+import { EuiButtonTo } from '../../../shared/react_router_helpers';
+import { WorkplaceSearchPageTemplate } from '../../components/layout';
import { ContentSection } from '../../components/shared/content_section';
import { SourcesTable } from '../../components/shared/sources_table';
-import { ViewContentHeader } from '../../components/shared/view_content_header';
+import { NAV } from '../../constants';
import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes';
import {
@@ -36,33 +35,41 @@ export const OrganizationSources: React.FC = () => {
const { dataLoading, contentSources } = useValues(SourcesLogic);
- if (dataLoading) return ;
-
- if (contentSources.length === 0) return ;
-
return (
-
-
-
- {ORG_SOURCES_LINK}
-
-
- }
- description={ORG_SOURCES_HEADER_DESCRIPTION}
- alignItems="flexStart"
- />
-
-
-
-
-
+
+ {ORG_SOURCES_LINK}
+ ,
+ ],
+ }
+ }
+ isLoading={dataLoading}
+ isEmptyState={!contentSources.length}
+ emptyState={ }
+ >
+
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx
index 08f560c984344..e2b0dfba1fa97 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx
@@ -15,7 +15,6 @@ import { shallow } from 'enzyme';
import { EuiCallOut, EuiEmptyPrompt } from '@elastic/eui';
-import { Loading } from '../../../shared/loading';
import { ContentSection } from '../../components/shared/content_section';
import { SourcesTable } from '../../components/shared/sources_table';
@@ -43,13 +42,6 @@ describe('PrivateSources', () => {
expect(wrapper.find(SourcesView)).toHaveLength(1);
});
- it('renders Loading when loading', () => {
- setMockValues({ ...mockValues, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('renders only shared sources section when canCreatePersonalSources is false', () => {
setMockValues({ ...mockValues });
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx
index 128c65eeb95da..693c1e8bd5e40 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx
@@ -13,12 +13,13 @@ import { EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { LicensingLogic } from '../../../shared/licensing';
-import { Loading } from '../../../shared/loading';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { AppLogic } from '../../app_logic';
import noSharedSourcesIcon from '../../assets/share_circle.svg';
+import { PersonalDashboardLayout } from '../../components/layout';
import { ContentSection } from '../../components/shared/content_section';
import { SourcesTable } from '../../components/shared/sources_table';
+import { NAV } from '../../constants';
import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes';
import { toSentenceSerial } from '../../utils';
@@ -53,8 +54,6 @@ export const PrivateSources: React.FC = () => {
account: { canCreatePersonalSources, groups },
} = useValues(AppLogic);
- if (dataLoading) return ;
-
const hasConfiguredConnectors = serviceTypes.some(({ configured }) => configured);
const canAddSources = canCreatePersonalSources && hasConfiguredConnectors;
const hasPrivateSources = privateContentSources?.length > 0;
@@ -144,10 +143,12 @@ export const PrivateSources: React.FC = () => {
);
return (
-
- {hasPrivateSources && !hasPlatinumLicense && licenseCallout}
- {canCreatePersonalSources && privateSourcesSection}
- {sharedSourcesSection}
-
+
+
+ {hasPrivateSources && !hasPlatinumLicense && licenseCallout}
+ {canCreatePersonalSources && privateSourcesSection}
+ {sharedSourcesSection}
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx
index 783fc434fe8e5..afe0d1f89faea 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx
@@ -5,21 +5,17 @@
* 2.0.
*/
-import '../../../__mocks__/shallow_useeffect.mock';
-
import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
-import { mockLocation, mockUseParams } from '../../../__mocks__/react_router';
+import { mockUseParams } from '../../../__mocks__/react_router';
import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock';
import { contentSources } from '../../__mocks__/content_sources.mock';
import React from 'react';
-import { Route, Switch } from 'react-router-dom';
+import { Route } from 'react-router-dom';
import { shallow } from 'enzyme';
-import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
-import { NAV } from '../../constants';
+import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout';
import { DisplaySettingsRouter } from './components/display_settings';
import { Overview } from './components/overview';
@@ -37,6 +33,7 @@ describe('SourceRouter', () => {
const mockValues = {
contentSource,
dataLoading: false,
+ isOrganization: true,
};
beforeEach(() => {
@@ -50,11 +47,41 @@ describe('SourceRouter', () => {
}));
});
- it('returns Loading when loading', () => {
- setMockValues({ ...mockValues, dataLoading: true });
- const wrapper = shallow( );
+ describe('mount/unmount events', () => {
+ it('fetches & initializes source data on mount', () => {
+ shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(initializeSource).toHaveBeenCalledWith(contentSource.id);
+ });
+
+ it('resets state on unmount', () => {
+ shallow( );
+ unmountHandler();
+
+ expect(resetSourceState).toHaveBeenCalled();
+ });
+ });
+
+ describe('loading state when fetching source data', () => {
+ // NOTE: The early page isLoading returns are required to prevent a flash of a completely empty
+ // page (instead of preserving the layout/side nav while loading). We also cannot let the code
+ // fall through to the router because some routes are conditionally rendered based on isCustomSource.
+
+ it('returns an empty loading Workplace Search page on organization views', () => {
+ setMockValues({ ...mockValues, dataLoading: true, isOrganization: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
+ expect(wrapper.prop('isLoading')).toEqual(true);
+ });
+
+ it('returns an empty loading personal dashboard page when not on an organization view', () => {
+ setMockValues({ ...mockValues, dataLoading: true, isOrganization: false });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(PersonalDashboardLayout);
+ expect(wrapper.prop('isLoading')).toEqual(true);
+ });
});
it('renders source routes (standard)', () => {
@@ -63,7 +90,6 @@ describe('SourceRouter', () => {
expect(wrapper.find(Overview)).toHaveLength(1);
expect(wrapper.find(SourceSettings)).toHaveLength(1);
expect(wrapper.find(SourceContent)).toHaveLength(1);
- expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(3);
});
@@ -76,55 +102,4 @@ describe('SourceRouter', () => {
expect(wrapper.find(SchemaChangeErrors)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(6);
});
-
- it('handles breadcrumbs while loading (standard)', () => {
- setMockValues({
- ...mockValues,
- contentSource: {},
- });
-
- const loadingBreadcrumbs = ['Sources', '...'];
-
- const wrapper = shallow( );
-
- const overviewBreadCrumb = wrapper.find(SetPageChrome).at(0);
- const contentBreadCrumb = wrapper.find(SetPageChrome).at(1);
- const settingsBreadCrumb = wrapper.find(SetPageChrome).at(2);
-
- expect(overviewBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs]);
- expect(contentBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.CONTENT]);
- expect(settingsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SETTINGS]);
- });
-
- it('handles breadcrumbs while loading (custom)', () => {
- setMockValues({
- ...mockValues,
- contentSource: { serviceType: 'custom' },
- });
-
- const loadingBreadcrumbs = ['Sources', '...'];
-
- const wrapper = shallow( );
-
- const schemaBreadCrumb = wrapper.find(SetPageChrome).at(2);
- const schemaErrorsBreadCrumb = wrapper.find(SetPageChrome).at(3);
- const displaySettingsBreadCrumb = wrapper.find(SetPageChrome).at(4);
-
- expect(schemaBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]);
- expect(schemaErrorsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]);
- expect(displaySettingsBreadCrumb.prop('trail')).toEqual([
- ...loadingBreadcrumbs,
- NAV.DISPLAY_SETTINGS,
- ]);
- });
-
- describe('reset state', () => {
- it('resets state when leaving source tree', () => {
- mockLocation.pathname = '/home';
- shallow( );
- unmountHandler();
-
- expect(resetSourceState).toHaveBeenCalled();
- });
- });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx
index d5d6c8e541e4f..bf68a60757c0d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx
@@ -10,18 +10,11 @@ import React, { useEffect } from 'react';
import { Route, Switch, useLocation, useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
-import moment from 'moment';
-import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
-
-import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
-import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
import { AppLogic } from '../../app_logic';
-import { NAV } from '../../constants';
+import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout';
import { CUSTOM_SERVICE_TYPE } from '../../constants';
import {
- ENT_SEARCH_LICENSE_MANAGEMENT,
REINDEX_JOB_PATH,
SOURCE_DETAILS_PATH,
SOURCE_CONTENT_PATH,
@@ -37,13 +30,7 @@ import { Overview } from './components/overview';
import { Schema } from './components/schema';
import { SchemaChangeErrors } from './components/schema/schema_change_errors';
import { SourceContent } from './components/source_content';
-import { SourceInfoCard } from './components/source_info_card';
import { SourceSettings } from './components/source_settings';
-import {
- SOURCE_DISABLED_CALLOUT_TITLE,
- SOURCE_DISABLED_CALLOUT_DESCRIPTION,
- SOURCE_DISABLED_CALLOUT_BUTTON,
-} from './constants';
import { SourceLogic } from './source_logic';
export const SourceRouter: React.FC = () => {
@@ -61,84 +48,43 @@ export const SourceRouter: React.FC = () => {
return resetSourceState;
}, []);
- if (dataLoading) return ;
+ if (dataLoading) {
+ return isOrganization ? (
+
+ ) : (
+
+ );
+ }
- const {
- name,
- createdAt,
- serviceType,
- serviceName,
- isFederatedSource,
- supportedByLicense,
- } = contentSource;
+ const { serviceType } = contentSource;
const isCustomSource = serviceType === CUSTOM_SERVICE_TYPE;
- const pageHeader = (
- <>
-
-
- >
- );
-
- const callout = (
- <>
-
- {SOURCE_DISABLED_CALLOUT_DESCRIPTION}
-
- {SOURCE_DISABLED_CALLOUT_BUTTON}
-
-
-
- >
- );
-
return (
- <>
- {!supportedByLicense && callout}
- {pageHeader}
-
-
-
-
-
+
+
+
+
+
+
+
+ {isCustomSource && (
+
+
-
-
-
-
+ )}
+ {isCustomSource && (
+
+
- {isCustomSource && (
-
-
-
-
-
- )}
- {isCustomSource && (
-
-
-
-
-
- )}
- {isCustomSource && (
-
-
-
-
-
- )}
-
-
-
-
+ )}
+ {isCustomSource && (
+
+
-
- >
+ )}
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx
index 84bff65e62cef..2abdba07b5c88 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx
@@ -11,12 +11,8 @@ import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
import { Location } from 'history';
import { useActions, useValues } from 'kea';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { LicensingLogic } from '../../../shared/licensing';
-import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
import { AppLogic } from '../../app_logic';
-import { NAV } from '../../constants';
import {
ADD_SOURCE_PATH,
SOURCE_DETAILS_PATH,
@@ -52,71 +48,53 @@ export const SourcesRouter: React.FC = () => {
}, [pathname]);
return (
- <>
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {staticSourceData.map(({ addPath, accountContextOnly }, i) => (
+
+ {!hasPlatinumLicense && accountContextOnly ? (
+
+ ) : (
+
+ )}
-
-
-
-
+ ))}
+ {staticSourceData.map(({ addPath }, i) => (
+
+
- {staticSourceData.map(({ addPath, accountContextOnly, name }, i) => (
-
-
- {!hasPlatinumLicense && accountContextOnly ? (
-
- ) : (
-
- )}
-
- ))}
- {staticSourceData.map(({ addPath, name }, i) => (
-
-
-
-
- ))}
- {staticSourceData.map(({ addPath, name }, i) => (
-
-
-
-
- ))}
- {staticSourceData.map(({ addPath, name, configuration: { needsConfiguration } }, i) => {
- if (needsConfiguration)
- return (
-
-
-
-
- );
- })}
- {canCreatePersonalSources ? (
-
-
-
-
-
- ) : (
-
- )}
-
-
-
+ ))}
+ {staticSourceData.map(({ addPath }, i) => (
+
+
-
-
+ ))}
+ {staticSourceData.map(({ addPath, configuration: { needsConfiguration } }, i) => {
+ if (needsConfiguration)
+ return (
+
+
+
+ );
+ })}
+ {canCreatePersonalSources ? (
+
+
-
- >
+ ) : (
+
+ )}
+
+
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx
index a4eb228eff92f..050aaf1dadf89 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx
@@ -11,8 +11,8 @@ import { useValues } from 'kea';
import { EuiTable, EuiTableBody, EuiTablePagination } from '@elastic/eui';
import { Pager } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
+import { USERNAME_LABEL, EMAIL_LABEL } from '../../../../shared/constants';
import { TableHeader } from '../../../../shared/table_header';
import { AppLogic } from '../../../app_logic';
import { UserRow } from '../../../components/shared/user_row';
@@ -20,27 +20,15 @@ import { User } from '../../../types';
import { GroupLogic } from '../group_logic';
const USERS_PER_PAGE = 10;
-const USERNAME_TABLE_HEADER = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader',
- {
- defaultMessage: 'Username',
- }
-);
-const EMAIL_TABLE_HEADER = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader',
- {
- defaultMessage: 'Email',
- }
-);
export const GroupUsersTable: React.FC = () => {
const { isFederatedAuth } = useValues(AppLogic);
const {
group: { users },
} = useValues(GroupLogic);
- const headerItems = [USERNAME_TABLE_HEADER];
+ const headerItems = [USERNAME_LABEL];
if (!isFederatedAuth) {
- headerItems.push(EMAIL_TABLE_HEADER);
+ headerItems.push(EMAIL_LABEL);
}
const [firstItem, setFirstItem] = useState(0);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx
index cf23470e8155e..7bd40d6f04a56 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx
@@ -25,6 +25,13 @@ describe('Overview', () => {
expect(mockActions.initializeOverview).toHaveBeenCalled();
});
+ it('does not render a page header when data is loading (to prevent a jump between non/onboarding headers)', () => {
+ setMockValues({ dataLoading: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('pageHeader')).toBeUndefined();
+ });
+
it('renders onboarding state', () => {
setMockValues({ dataLoading: false });
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx
index 0049c5b732d3d..c51fdb64b8f26 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx
@@ -53,17 +53,15 @@ export const Overview: React.FC = () => {
const hideOnboarding = hasUsers && hasOrgSources && isOldAccount && orgName !== defaultOrgName;
- const headerTitle = dataLoading || hideOnboarding ? HEADER_TITLE : ONBOARDING_HEADER_TITLE;
- const headerDescription =
- dataLoading || hideOnboarding ? HEADER_DESCRIPTION : ONBOARDING_HEADER_DESCRIPTION;
+ const headerTitle = hideOnboarding ? HEADER_TITLE : ONBOARDING_HEADER_TITLE;
+ const headerDescription = hideOnboarding ? HEADER_DESCRIPTION : ONBOARDING_HEADER_DESCRIPTION;
return (
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts
index 92c8b7827b9b6..809b631c78391 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts
@@ -7,14 +7,6 @@
import { i18n } from '@kbn/i18n';
-export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage',
- {
- defaultMessage:
- 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.',
- }
-);
-
export const ROLE_MAPPING_DELETED_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.roleMappingDeletedMessage',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx
index b153d01224193..01d32bec14ebd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx
@@ -10,9 +10,14 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants';
-import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping';
+import {
+ RoleMappingsTable,
+ RoleMappingsHeading,
+ RolesEmptyPrompt,
+} from '../../../shared/role_mapping';
import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants';
import { WorkplaceSearchPageTemplate } from '../../components/layout';
+import { SECURITY_DOCS_URL } from '../../routes';
import { ROLE_MAPPINGS_TABLE_HEADER } from './constants';
@@ -20,9 +25,12 @@ import { RoleMapping } from './role_mapping';
import { RoleMappingsLogic } from './role_mappings_logic';
export const RoleMappings: React.FC = () => {
- const { initializeRoleMappings, initializeRoleMapping, handleDeleteMapping } = useActions(
- RoleMappingsLogic
- );
+ const {
+ enableRoleBasedAccess,
+ initializeRoleMappings,
+ initializeRoleMapping,
+ handleDeleteMapping,
+ } = useActions(RoleMappingsLogic);
const {
roleMappings,
@@ -35,10 +43,19 @@ export const RoleMappings: React.FC = () => {
initializeRoleMappings();
}, []);
+ const rolesEmptyState = (
+
+ );
+
const roleMappingsSection = (
initializeRoleMapping()}
/>
{
pageChrome={[ROLE_MAPPINGS_TITLE]}
pageHeader={{ pageTitle: ROLE_MAPPINGS_TITLE }}
isLoading={dataLoading}
+ isEmptyState={roleMappings.length < 1}
+ emptyState={rolesEmptyState}
>
{roleMappingFlyoutOpen && }
{roleMappingsSection}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
index 4ee530870284e..a4bbddbd23b49 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
@@ -90,6 +90,13 @@ describe('RoleMappingsLogic', () => {
expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id]));
});
+ it('setRoleMappings', () => {
+ RoleMappingsLogic.actions.setRoleMappings({ roleMappings: [wsRoleMapping] });
+
+ expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]);
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
+ });
+
it('handleRoleChange', () => {
RoleMappingsLogic.actions.handleRoleChange('user');
@@ -234,6 +241,30 @@ describe('RoleMappingsLogic', () => {
});
describe('listeners', () => {
+ describe('enableRoleBasedAccess', () => {
+ it('calls API and sets values', async () => {
+ const setRoleMappingsSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappings');
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
+ RoleMappingsLogic.actions.enableRoleBasedAccess();
+
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(true);
+
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/workplace_search/org/role_mappings/enable_role_based_access'
+ );
+ await nextTick();
+ expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps);
+ });
+
+ it('handles error', async () => {
+ http.post.mockReturnValue(Promise.reject('this is an error'));
+ RoleMappingsLogic.actions.enableRoleBasedAccess();
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
+ });
+ });
+
describe('initializeRoleMappings', () => {
it('calls API and sets values', async () => {
const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData');
@@ -351,18 +382,8 @@ describe('RoleMappingsLogic', () => {
});
describe('handleDeleteMapping', () => {
- let confirmSpy: any;
const roleMappingId = 'r1';
- beforeEach(() => {
- confirmSpy = jest.spyOn(window, 'confirm');
- confirmSpy.mockImplementation(jest.fn(() => true));
- });
-
- afterEach(() => {
- confirmSpy.mockRestore();
- });
-
it('calls API and refreshes list', async () => {
const initializeRoleMappingsSpy = jest.spyOn(
RoleMappingsLogic.actions,
@@ -388,15 +409,6 @@ describe('RoleMappingsLogic', () => {
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
});
-
- it('will do nothing if not confirmed', async () => {
- RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
- window.confirm = () => false;
- RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
-
- expect(http.delete).not.toHaveBeenCalled();
- await nextTick();
- });
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
index 361425b7a78a1..76b41b2f383eb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
@@ -20,7 +20,6 @@ import { AttributeName } from '../../../shared/types';
import { RoleGroup, WSRoleMapping, Role } from '../../types';
import {
- DELETE_ROLE_MAPPING_MESSAGE,
ROLE_MAPPING_DELETED_MESSAGE,
ROLE_MAPPING_CREATED_MESSAGE,
ROLE_MAPPING_UPDATED_MESSAGE,
@@ -57,10 +56,16 @@ interface RoleMappingsActions {
initializeRoleMappings(): void;
resetState(): void;
setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping };
+ setRoleMappings({
+ roleMappings,
+ }: {
+ roleMappings: WSRoleMapping[];
+ }): { roleMappings: WSRoleMapping[] };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
openRoleMappingFlyout(): void;
closeRoleMappingFlyout(): void;
setRoleMappingErrors(errors: string[]): { errors: string[] };
+ enableRoleBasedAccess(): void;
}
interface RoleMappingsValues {
@@ -88,6 +93,7 @@ export const RoleMappingsLogic = kea data,
setRoleMapping: (roleMapping: WSRoleMapping) => ({ roleMapping }),
+ setRoleMappings: ({ roleMappings }: { roleMappings: WSRoleMapping[] }) => ({ roleMappings }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string[]) => ({ value }),
handleRoleChange: (roleType: Role) => ({ roleType }),
@@ -98,6 +104,7 @@ export const RoleMappingsLogic = kea ({ value }),
handleAllGroupsSelectionChange: (selected: boolean) => ({ selected }),
+ enableRoleBasedAccess: true,
resetState: true,
initializeRoleMappings: true,
initializeRoleMapping: (roleMappingId?: string) => ({ roleMappingId }),
@@ -111,13 +118,16 @@ export const RoleMappingsLogic = kea false,
+ setRoleMappings: () => false,
resetState: () => true,
+ enableRoleBasedAccess: () => true,
},
],
roleMappings: [
[],
{
setRoleMappingsData: (_, { roleMappings }) => roleMappings,
+ setRoleMappings: (_, { roleMappings }) => roleMappings,
resetState: () => [],
},
],
@@ -260,6 +270,17 @@ export const RoleMappingsLogic = kea ({
+ enableRoleBasedAccess: async () => {
+ const { http } = HttpLogic.values;
+ const route = '/api/workplace_search/org/role_mappings/enable_role_based_access';
+
+ try {
+ const response = await http.post(route);
+ actions.setRoleMappings(response);
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
initializeRoleMappings: async () => {
const { http } = HttpLogic.values;
const route = '/api/workplace_search/org/role_mappings';
@@ -279,14 +300,12 @@ export const RoleMappingsLogic = kea {
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx
index b32e3af021827..35619d2b2d560 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx
@@ -40,6 +40,13 @@ describe('SourceConfig', () => {
expect(wrapper.find(EuiConfirmModal)).toHaveLength(1);
});
+ it('renders a breadcrumb fallback while data is loading', () => {
+ setMockValues({ dataLoading: true, sourceConfigData: {} });
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('pageChrome')).toEqual(['Settings', 'Content source connectors', '...']);
+ });
+
it('handles delete click', () => {
const wrapper = shallow( );
const saveConfig = wrapper.find(SaveConfig);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx
index f1dfda78ee13f..c2a0b60e1eca3 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx
@@ -47,7 +47,7 @@ export const SourceConfig: React.FC = ({ sourceIndex }) => {
return (
{
'ui_error.cannot_connect': 3,
'ui_error.not_found': 7,
'ui_clicked.create_first_engine_button': 40,
- 'ui_clicked.header_launch_button': 50,
- 'ui_clicked.engine_table_link': 60,
+ 'ui_clicked.engine_table_link': 50,
},
}),
incrementCounter: jest.fn(),
@@ -66,8 +65,7 @@ describe('App Search Telemetry Usage Collector', () => {
},
ui_clicked: {
create_first_engine_button: 40,
- header_launch_button: 50,
- engine_table_link: 60,
+ engine_table_link: 50,
},
});
});
@@ -93,7 +91,6 @@ describe('App Search Telemetry Usage Collector', () => {
},
ui_clicked: {
create_first_engine_button: 0,
- header_launch_button: 0,
engine_table_link: 0,
},
});
diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
index 36ba2976f929a..4dca6ed58e0c5 100644
--- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
+++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
@@ -23,7 +23,6 @@ interface Telemetry {
};
ui_clicked: {
create_first_engine_button: number;
- header_launch_button: number;
engine_table_link: number;
};
}
@@ -54,7 +53,6 @@ export const registerTelemetryUsageCollector = (
},
ui_clicked: {
create_first_engine_button: { type: 'long' },
- header_launch_button: { type: 'long' },
engine_table_link: { type: 'long' },
},
},
@@ -85,7 +83,6 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log
},
ui_clicked: {
create_first_engine_button: 0,
- header_launch_button: 0,
engine_table_link: 0,
},
};
@@ -110,7 +107,6 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log
'ui_clicked.create_first_engine_button',
0
),
- header_launch_button: get(savedObjectAttributes, 'ui_clicked.header_launch_button', 0),
engine_table_link: get(savedObjectAttributes, 'ui_clicked.engine_table_link', 0),
},
} as Telemetry;
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
index 718597c12e9c5..7d9f08627516b 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
@@ -7,7 +7,11 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import { registerRoleMappingsRoute, registerRoleMappingRoute } from './role_mappings';
+import {
+ registerEnableRoleMappingsRoute,
+ registerRoleMappingsRoute,
+ registerRoleMappingRoute,
+} from './role_mappings';
const roleMappingBaseSchema = {
rules: { username: 'user' },
@@ -18,6 +22,29 @@ const roleMappingBaseSchema = {
};
describe('role mappings routes', () => {
+ describe('POST /api/app_search/role_mappings/enable_role_based_access', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/app_search/role_mappings/enable_role_based_access',
+ });
+
+ registerEnableRoleMappingsRoute({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request handler', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/role_mappings/enable_role_based_access',
+ });
+ });
+ });
+
describe('GET /api/app_search/role_mappings', () => {
let mockRouter: MockRouter;
@@ -36,7 +63,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings',
+ path: '/as/role_mappings',
});
});
});
@@ -59,7 +86,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings',
+ path: '/as/role_mappings',
});
});
@@ -94,7 +121,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
});
});
@@ -129,7 +156,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
});
});
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
index 75724a3344d6d..da620be2ea950 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
@@ -17,6 +17,21 @@ const roleMappingBaseSchema = {
authProvider: schema.arrayOf(schema.string()),
};
+export function registerEnableRoleMappingsRoute({
+ router,
+ enterpriseSearchRequestHandler,
+}: RouteDependencies) {
+ router.post(
+ {
+ path: '/api/app_search/role_mappings/enable_role_based_access',
+ validate: false,
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/as/role_mappings/enable_role_based_access',
+ })
+ );
+}
+
export function registerRoleMappingsRoute({
router,
enterpriseSearchRequestHandler,
@@ -27,7 +42,7 @@ export function registerRoleMappingsRoute({
validate: false,
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings',
+ path: '/as/role_mappings',
})
);
@@ -39,7 +54,7 @@ export function registerRoleMappingsRoute({
},
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings',
+ path: '/as/role_mappings',
})
);
}
@@ -59,7 +74,7 @@ export function registerRoleMappingRoute({
},
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
})
);
@@ -73,12 +88,13 @@ export function registerRoleMappingRoute({
},
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
})
);
}
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
+ registerEnableRoleMappingsRoute(dependencies);
registerRoleMappingsRoute(dependencies);
registerRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
index a945866da5ef2..aa0e9983166c0 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
@@ -7,9 +7,36 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import { registerOrgRoleMappingsRoute, registerOrgRoleMappingRoute } from './role_mappings';
+import {
+ registerOrgEnableRoleMappingsRoute,
+ registerOrgRoleMappingsRoute,
+ registerOrgRoleMappingRoute,
+} from './role_mappings';
describe('role mappings routes', () => {
+ describe('POST /api/workplace_search/org/role_mappings/enable_role_based_access', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/workplace_search/org/role_mappings/enable_role_based_access',
+ });
+
+ registerOrgEnableRoleMappingsRoute({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request handler', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/ws/org/role_mappings/enable_role_based_access',
+ });
+ });
+ });
+
describe('GET /api/workplace_search/org/role_mappings', () => {
let mockRouter: MockRouter;
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
index a0fcec63cbb27..cea7bcb311ce8 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
@@ -17,6 +17,21 @@ const roleMappingBaseSchema = {
authProvider: schema.arrayOf(schema.string()),
};
+export function registerOrgEnableRoleMappingsRoute({
+ router,
+ enterpriseSearchRequestHandler,
+}: RouteDependencies) {
+ router.post(
+ {
+ path: '/api/workplace_search/org/role_mappings/enable_role_based_access',
+ validate: false,
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/ws/org/role_mappings/enable_role_based_access',
+ })
+ );
+}
+
export function registerOrgRoleMappingsRoute({
router,
enterpriseSearchRequestHandler,
@@ -79,6 +94,7 @@ export function registerOrgRoleMappingRoute({
}
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
+ registerOrgEnableRoleMappingsRoute(dependencies);
registerOrgRoleMappingsRoute(dependencies);
registerOrgRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md
index 032f77543acb9..ffbd20dd6f2be 100644
--- a/x-pack/plugins/event_log/README.md
+++ b/x-pack/plugins/event_log/README.md
@@ -131,7 +131,7 @@ Below is a document in the expected structure, with descriptions of the fields:
instance_id: "alert instance id, for relevant documents",
action_group_id: "alert action group, for relevant documents",
action_subgroup: "alert action subgroup, for relevant documents",
- status: "overall alert status, after alert execution",
+ status: "overall alert status, after rule execution",
},
saved_objects: [
{
@@ -160,21 +160,26 @@ plugins:
- `action: execute-via-http` - generated when an action is executed via HTTP request
- `provider: alerting`
- - `action: execute` - generated when an alert executor runs
- - `action: execute-action` - generated when an alert schedules an action to run
- - `action: new-instance` - generated when an alert has a new instance id that is active
- - `action: recovered-instance` - generated when an alert has a previously active instance id that is no longer active
- - `action: active-instance` - generated when an alert determines an instance id is active
+ - `action: execute` - generated when a rule executor runs
+ - `action: execute-action` - generated when a rule schedules an action to run
+ - `action: new-instance` - generated when a rule has a new instance id that is active
+ - `action: recovered-instance` - generated when a rule has a previously active instance id that is no longer active
+ - `action: active-instance` - generated when a rule determines an instance id is active
For the `saved_objects` array elements, these are references to saved objects
-associated with the event. For the `alerting` provider, those are alert saved
-ojects and for the `actions` provider those are action saved objects. The
-`alerts:execute-action` event includes both the alert and action saved object
-references. For that event, only the alert reference has the optional `rel`
+associated with the event. For the `alerting` provider, those are rule saved
+ojects and for the `actions` provider those are connector saved objects. The
+`alerts:execute-action` event includes both the rule and connector saved object
+references. For that event, only the rule reference has the optional `rel`
property with a `primary` value. This property is used when searching the
event log to indicate which saved objects should be directly searchable via
-saved object references. For the `alerts:execute-action` event, searching
-only via the alert saved object reference will return the event.
+saved object references. For the `alerts:execute-action` event, only searching
+via the rule saved object reference will return the event; searching via the
+connector save object reference will **NOT** return the event. The
+`actions:execute` event also includes both the rule and connector saved object
+references, and both of them have the `rel` property with a `primary` value,
+allowing those events to be returned in searches of either the rule or
+connector.
## Event Log index - associated resources
diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts
index e9dd968d3f048..81ea2a630d3db 100644
--- a/x-pack/plugins/fleet/common/constants/epm.ts
+++ b/x-pack/plugins/fleet/common/constants/epm.ts
@@ -48,6 +48,9 @@ export const dataTypes = {
Metrics: 'metrics',
} as const;
+// currently identical but may be a subset or otherwise different some day
+export const monitoringTypes = Object.values(dataTypes);
+
export const installationStatuses = {
Installed: 'installed',
NotInstalled: 'not_installed',
diff --git a/x-pack/plugins/fleet/common/constants/preconfiguration.ts b/x-pack/plugins/fleet/common/constants/preconfiguration.ts
index 937c08b7e8cb5..2ec67393df76b 100644
--- a/x-pack/plugins/fleet/common/constants/preconfiguration.ts
+++ b/x-pack/plugins/fleet/common/constants/preconfiguration.ts
@@ -12,6 +12,7 @@ import {
FLEET_SYSTEM_PACKAGE,
FLEET_SERVER_PACKAGE,
autoUpdatePackages,
+ monitoringTypes,
} from './epm';
export const PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE =
@@ -40,7 +41,7 @@ export const DEFAULT_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = {
],
is_default: true,
is_managed: false,
- monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
+ monitoring_enabled: monitoringTypes,
};
export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = {
@@ -58,7 +59,7 @@ export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefa
is_default: false,
is_default_fleet_server: true,
is_managed: false,
- monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
+ monitoring_enabled: monitoringTypes,
};
export const DEFAULT_PACKAGES = defaultPackages.map((name) => ({
diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts
index 037c0ee506a05..0b892bacf53a7 100644
--- a/x-pack/plugins/fleet/common/constants/routes.ts
+++ b/x-pack/plugins/fleet/common/constants/routes.ts
@@ -117,5 +117,5 @@ export const INSTALL_SCRIPT_API_ROUTES = `${API_ROOT}/install/{osType}`;
// Policy preconfig API routes
export const PRECONFIGURATION_API_ROUTES = {
- PUT_PRECONFIG: `${API_ROOT}/setup/preconfiguration`,
+ UPDATE_PATTERN: `${API_ROOT}/setup/preconfiguration`,
};
diff --git a/x-pack/plugins/fleet/common/services/agent_status.ts b/x-pack/plugins/fleet/common/services/agent_status.ts
index df5de6ad98191..b8a59e6447723 100644
--- a/x-pack/plugins/fleet/common/services/agent_status.ts
+++ b/x-pack/plugins/fleet/common/services/agent_status.ts
@@ -54,7 +54,7 @@ export function buildKueryForOnlineAgents() {
}
export function buildKueryForErrorAgents() {
- return 'last_checkin_status:error or .last_checkin_status:degraded';
+ return 'last_checkin_status:error or last_checkin_status:degraded';
}
export function buildKueryForOfflineAgents() {
diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.test.ts b/x-pack/plugins/fleet/common/services/hosts_utils.test.ts
similarity index 100%
rename from x-pack/plugins/fleet/server/services/hosts_utils.test.ts
rename to x-pack/plugins/fleet/common/services/hosts_utils.test.ts
diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.ts b/x-pack/plugins/fleet/common/services/hosts_utils.ts
similarity index 100%
rename from x-pack/plugins/fleet/server/services/hosts_utils.ts
rename to x-pack/plugins/fleet/common/services/hosts_utils.ts
diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts
index 86361ae163399..a6f4cd319b970 100644
--- a/x-pack/plugins/fleet/common/services/index.ts
+++ b/x-pack/plugins/fleet/common/services/index.ts
@@ -30,3 +30,5 @@ export {
validationHasErrors,
countValidationErrors,
} from './validate_package_policy';
+
+export { normalizeHostsForAgents } from './hosts_utils';
diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts
index 95f91165aaf94..59691bf32d099 100644
--- a/x-pack/plugins/fleet/common/types/index.ts
+++ b/x-pack/plugins/fleet/common/types/index.ts
@@ -25,6 +25,7 @@ export interface FleetConfigType {
};
agentPolicies?: PreconfiguredAgentPolicy[];
packages?: PreconfiguredPackage[];
+ agentIdVerificationEnabled?: boolean;
}
// Calling Object.entries(PackagesGroupedByStatus) gave `status: string`
diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts
index a9393abcc57ef..f64467ca674fb 100644
--- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts
+++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts
@@ -6,7 +6,7 @@
*/
import type { agentPolicyStatuses } from '../../constants';
-import type { DataType, ValueOf } from '../../types';
+import type { MonitoringType, ValueOf } from '../../types';
import type { PackagePolicy, PackagePolicyPackage } from './package_policy';
import type { Output } from './output';
@@ -20,7 +20,8 @@ export interface NewAgentPolicy {
is_default?: boolean;
is_default_fleet_server?: boolean; // Optional when creating a policy
is_managed?: boolean; // Optional when creating a policy
- monitoring_enabled?: Array>;
+ monitoring_enabled?: MonitoringType;
+ unenroll_timeout?: number;
is_preconfigured?: boolean;
}
@@ -138,4 +139,8 @@ export interface FleetServerPolicy {
* True when this policy is the default policy to start Fleet Server
*/
default_fleet_server: boolean;
+ /**
+ * Auto unenroll any Elastic Agents which have not checked in for this many seconds
+ */
+ unenroll_timeout?: number;
}
diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts
index 83875801300d3..36554b8409364 100644
--- a/x-pack/plugins/fleet/common/types/models/epm.ts
+++ b/x-pack/plugins/fleet/common/types/models/epm.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type { estypes } from '@elastic/elasticsearch';
// Follow pattern from https://github.com/elastic/kibana/pull/52447
// TODO: Update when https://github.com/elastic/kibana/issues/53021 is closed
import type { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/public';
@@ -13,6 +14,7 @@ import type {
ASSETS_SAVED_OBJECT_TYPE,
agentAssetTypes,
dataTypes,
+ monitoringTypes,
installationStatuses,
} from '../../constants';
import type { ValueOf } from '../../types';
@@ -43,7 +45,7 @@ export type InstallSource = 'registry' | 'upload';
export type EpmPackageInstallStatus = 'installed' | 'installing';
-export type DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom';
+export type DetailViewPanelName = 'overview' | 'policies' | 'assets' | 'settings' | 'custom';
export type ServiceName = 'kibana' | 'elasticsearch';
export type AgentAssetType = typeof agentAssetTypes;
export type DocAssetType = 'doc' | 'notice';
@@ -91,7 +93,7 @@ export enum ElasticsearchAssetType {
}
export type DataType = typeof dataTypes;
-
+export type MonitoringType = typeof monitoringTypes;
export type InstallablePackage = RegistryPackage | ArchivePackage;
export type ArchivePackage = PackageSpecManifest &
@@ -299,8 +301,8 @@ export interface RegistryDataStream {
}
export interface RegistryElasticsearch {
- 'index_template.settings'?: object;
- 'index_template.mappings'?: object;
+ 'index_template.settings'?: estypes.IndicesIndexSettings;
+ 'index_template.mappings'?: estypes.MappingTypeMapping;
}
export interface RegistryDataStreamPermissions {
@@ -425,7 +427,7 @@ export interface IndexTemplate {
_meta: object;
}
-export interface TemplateRef {
+export interface IndexTemplateEntry {
templateName: string;
indexTemplate: IndexTemplate;
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
index a44edb5ce9a42..633f8a2c57409 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
@@ -21,6 +21,7 @@ import {
EuiCheckboxGroup,
EuiButton,
EuiLink,
+ EuiFieldNumber,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -29,6 +30,7 @@ import styled from 'styled-components';
import { dataTypes } from '../../../../../../common';
import type { NewAgentPolicy, AgentPolicy } from '../../../types';
import { isValidNamespace } from '../../../services';
+import { useStartServices } from '../../../hooks';
import { AgentPolicyDeleteProvider } from './agent_policy_delete_provider';
@@ -83,6 +85,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({
isEditing = false,
onDelete = () => {},
}) => {
+ const { docLinks } = useStartServices();
const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({});
const fields: Array<{
name: 'name' | 'description' | 'namespace';
@@ -156,6 +159,10 @@ export const AgentPolicyForm: React.FunctionComponent = ({
);
});
+ const unenrollmentTimeoutText = i18n.translate(
+ 'xpack.fleet.agentPolicyForm.unenrollmentTimeoutLabel',
+ { defaultMessage: 'Unenrollment timeout' }
+ );
const advancedOptionsContent = (
<>
@@ -174,10 +181,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({
defaultMessage="Namespaces are a user-configurable arbitrary grouping that makes it easier to search for data and manage user permissions. A policy namespace is used to name its integration's data streams. {fleetUserGuide}."
values={{
fleetUserGuide: (
-
+
{i18n.translate(
'xpack.fleet.agentPolicyForm.nameSpaceFieldDescription.fleetUserGuideLabel',
{ defaultMessage: 'Learn more' }
@@ -298,6 +302,27 @@ export const AgentPolicyForm: React.FunctionComponent = ({
}}
/>
+ {unenrollmentTimeoutText}}
+ description={
+
+ }
+ >
+
+ updateAgentPolicy({ unenroll_timeout: Number(e.target.value) })}
+ isInvalid={Boolean(touchedFields.unenroll_timeout && validation.unenroll_timeout)}
+ onBlur={() => setTouchedFields({ ...touchedFields, unenroll_timeout: true })}
+ placeholder={unenrollmentTimeoutText}
+ />
+
+
{isEditing &&
'id' in agentPolicy &&
!agentPolicy.is_managed &&
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
index 75fc06c1a4494..b3b0d6ed51cb4 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
@@ -19,10 +19,12 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
+ EuiLink,
} from '@elastic/eui';
import type { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import type { ApplicationStart } from 'kibana/public';
+import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public';
import type {
AgentPolicy,
PackageInfo,
@@ -60,7 +62,7 @@ const StepsWithLessPadding = styled(EuiSteps)`
`;
const CustomEuiBottomBar = styled(EuiBottomBar)`
- // Set a relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar
+ /* A relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar */
z-index: 50;
`;
@@ -84,11 +86,26 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
const history = useHistory();
const handleNavigateTo = useNavigateToCallback();
const routeState = useIntraAppState();
- const from: CreatePackagePolicyFrom = 'policyId' in params ? 'policy' : 'package';
const { search } = useLocation();
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
- const policyId = useMemo(() => queryParams.get('policyId') ?? undefined, [queryParams]);
+ const queryParamsPolicyId = useMemo(() => queryParams.get('policyId') ?? undefined, [
+ queryParams,
+ ]);
+
+ /**
+ * Please note: policyId can come from one of two sources. The URL param (in the URL path) or
+ * in the query params (?policyId=foo).
+ *
+ * Either way, we take this as an indication that a user is "coming from" the fleet policy UI
+ * since we link them out to packages (a.k.a. integrations) UI when choosing a new package. It is
+ * no longer possible to choose a package directly in the create package form.
+ *
+ * We may want to deprecate the ability to pass in policyId from URL params since there is no package
+ * creation possible if a user has not chosen one from the packages UI.
+ */
+ const from: CreatePackagePolicyFrom =
+ 'policyId' in params || queryParamsPolicyId ? 'policy' : 'package';
// Agent policy and package info states
const [agentPolicy, setAgentPolicy] = useState();
@@ -280,6 +297,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
);
}
+ const fromPolicyWithoutAgentsAssigned = from === 'policy' && agentPolicy && agentCount === 0;
+
+ const fromPackageWithoutAgentsAssigned =
+ from === 'package' && packageInfo && agentPolicy && agentCount === 0;
+
+ const hasAgentsAssigned = agentCount && agentPolicy;
+
notifications.toasts.addSuccess({
title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', {
defaultMessage: `'{packagePolicyName}' integration added.`,
@@ -287,22 +311,47 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
packagePolicyName: packagePolicy.name,
},
}),
- text:
- agentCount && agentPolicy
- ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', {
- defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`,
- values: {
- agentPolicyName: agentPolicy.name,
- },
- })
- : (params as AddToPolicyParams)?.policyId && agentPolicy && agentCount === 0
- ? i18n.translate('xpack.fleet.createPackagePolicy.addAgentNextNotification', {
+ text: fromPolicyWithoutAgentsAssigned
+ ? i18n.translate(
+ 'xpack.fleet.createPackagePolicy.policyContextAddAgentNextNotificationMessage',
+ {
defaultMessage: `The policy has been updated. Add an agent to the '{agentPolicyName}' policy to deploy this policy.`,
values: {
- agentPolicyName: agentPolicy.name,
+ agentPolicyName: agentPolicy!.name,
},
- })
- : undefined,
+ }
+ )
+ : fromPackageWithoutAgentsAssigned
+ ? toMountPoint(
+ // To render the link below we need to mount this JSX in the success toast
+
+ {i18n.translate(
+ 'xpack.fleet.createPackagePolicy.integrationsContextAddAgentLinkMessage',
+ { defaultMessage: 'add an agent' }
+ )}
+
+ ),
+ }}
+ />
+ )
+ : hasAgentsAssigned
+ ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', {
+ defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`,
+ values: {
+ agentPolicyName: agentPolicy!.name,
+ },
+ })
+ : undefined,
'data-test-subj': 'packagePolicyCreateSuccessToast',
});
} else {
@@ -312,6 +361,9 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
setFormState('VALID');
}
}, [
+ getHref,
+ from,
+ packageInfo,
agentCount,
agentPolicy,
formState,
@@ -353,13 +405,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
),
- [params, updatePackageInfo, agentPolicy, updateAgentPolicy, policyId]
+ [params, updatePackageInfo, agentPolicy, updateAgentPolicy, queryParamsPolicyId]
);
const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create');
@@ -455,7 +507,8 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
)}
-
+
+
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx
index 7444bed6ed3fd..c276e67cabbff 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx
@@ -29,6 +29,7 @@ import type {
} from '../../../types';
import { packageToPackagePolicy, pkgKeyFromPackageInfo } from '../../../services';
import { Loading } from '../../../components';
+import { useStartServices } from '../../../hooks';
import { isAdvancedVar } from './services';
import type { PackagePolicyValidationResults } from './services';
@@ -52,6 +53,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
validationResults,
submitAttempted,
}) => {
+ const { docLinks } = useStartServices();
// Form show/hide states
const [isShowingAdvanced, setIsShowingAdvanced] = useState(false);
@@ -167,10 +169,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
defaultMessage="Change the default namespace inherited from the selected Agent policy. This setting changes the name of the integration's data stream. {learnMore}."
values={{
learnMore: (
-
+
{i18n.translate(
'xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceHelpLearnMoreLabel',
{ defaultMessage: 'Learn more' }
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx
index 54adbd78ab75a..39340a21d349b 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx
@@ -9,10 +9,11 @@ import React, { memo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
-import { useCapabilities, useLink } from '../../../../../hooks';
+import { useCapabilities, useStartServices } from '../../../../../hooks';
+import { pagePathGetters, INTEGRATIONS_PLUGIN_ID } from '../../../../../constants';
export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => {
- const { getHref } = useLink();
+ const { application } = useStartServices();
const hasWriteCapabilities = useCapabilities().write;
return (
@@ -36,7 +37,12 @@ export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => {
+ application.navigateToApp(INTEGRATIONS_PLUGIN_ID, {
+ path: `#${pagePathGetters.integrations_all()[1]}`,
+ state: { forAgentPolicyId: policyId },
+ })
+ }
>
(
setIsLoading(true);
try {
// eslint-disable-next-line @typescript-eslint/naming-convention
- const { name, description, namespace, monitoring_enabled } = agentPolicy;
+ const { name, description, namespace, monitoring_enabled, unenroll_timeout } = agentPolicy;
const { data, error } = await sendUpdateAgentPolicy(agentPolicy.id, {
name,
description,
namespace,
monitoring_enabled,
+ unenroll_timeout,
});
if (data) {
notifications.toasts.addSuccess(
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
index 33dbbb25c5d42..5992888564e7f 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
@@ -5,7 +5,9 @@
* 2.0.
*/
+import type { ReactNode } from 'react';
import React, { useState } from 'react';
+import type { StyledComponent } from 'styled-components';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -29,7 +31,13 @@ import type { NewAgentPolicy, AgentPolicy } from '../../../../types';
import { useCapabilities, useStartServices, sendCreateAgentPolicy } from '../../../../hooks';
import { AgentPolicyForm, agentPolicyFormValidation } from '../../components';
-const FlyoutWithHigherZIndex = styled(EuiFlyout)`
+// TODO: EUI team follow up on complex types and styled-components `styled`
+// https://github.com/elastic/eui/issues/4855
+const FlyoutWithHigherZIndex: StyledComponent<
+ typeof EuiFlyout,
+ {},
+ { children?: ReactNode }
+> = styled(EuiFlyout)`
z-index: ${(props) => props.theme.eui.euiZLevel5};
`;
@@ -39,6 +47,7 @@ interface Props extends EuiFlyoutProps {
export const CreateAgentPolicyFlyout: React.FunctionComponent = ({
onClose,
+ as,
...restOfProps
}) => {
const { notifications } = useStartServices();
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx
index 5335432a13613..b4e6f1007536f 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx
@@ -24,6 +24,7 @@ import {
import { WithoutHeaderLayout } from '../../../layouts';
import type { GetFleetStatusResponse } from '../../../types';
+import { useStartServices } from '../../../hooks';
export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> = ({
isMissing,
@@ -50,6 +51,8 @@ export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> =
export const MissingESRequirementsPage: React.FunctionComponent<{
missingRequirements: GetFleetStatusResponse['missing_requirements'];
}> = ({ missingRequirements }) => {
+ const { docLinks } = useStartServices();
+
return (
@@ -79,7 +82,7 @@ export const MissingESRequirementsPage: React.FunctionComponent<{
values={{
esSecurityLink: (
@@ -104,7 +107,7 @@ export const MissingESRequirementsPage: React.FunctionComponent<{
true: true ,
apiKeyLink: (
@@ -128,11 +131,7 @@ xpack.security.authc.api_key.enabled: true`}
defaultMessage="For more information, read our {link} guide."
values={{
link: (
-
+
void;
}): EuiStepProps => {
+ const { docLinks } = useStartServices();
+
return {
title: i18n.translate('xpack.fleet.fleetServerSetup.stepInstallAgentTitle', {
defaultMessage: 'Start Fleet Server',
@@ -147,7 +149,11 @@ export const FleetServerCommandStep = ({
defaultMessage="From the agent directory, copy and run the appropriate quick start command to start an Elastic Agent as a Fleet Server using the generated token and a self-signed certificate. See the {userGuideLink} for instructions on using your own certificates for production deployment. All commands require administrator privileges."
values={{
userGuideLink: (
-
+
+
{
platform,
setPlatform,
} = useFleetServerInstructions();
+ const { docLinks } = useStartServices();
return (
@@ -304,7 +307,11 @@ const OnPremInstructions: React.FC = () => {
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. See the {userGuideLink} for more information."
values={{
userGuideLink: (
-
+
{
};
const CloudInstructions: React.FC<{ deploymentUrl: string }> = ({ deploymentUrl }) => {
+ const { docLinks } = useStartServices();
+
return (
= ({ deploymentUrl
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. You can add one to your deployment by enabling APM & Fleet. For more information see the {link}"
values={{
link: (
-
+
= ({ onClose }) => {
const { getAssetsPath } = useLink();
- const { notifications, cloud } = useStartServices();
+ const { notifications, cloud, docLinks } = useStartServices();
const isCloud = !!cloud?.cloudId;
@@ -163,7 +163,11 @@ export const FleetServerUpgradeModal: React.FunctionComponent = ({ onClos
),
link: (
-
+
{
+ const { toAssets } = useLinks();
+ const theme = useTheme() as EuiTheme;
+ const IS_DARK_THEME = theme.darkMode;
+
+ return (
+
+ );
+});
+
export const DefaultLayout: React.FunctionComponent = memo(({ section, children }) => {
const { getHref } = useLink();
@@ -27,11 +57,29 @@ export const DefaultLayout: React.FunctionComponent = memo(({ section, ch
}
leftColumn={
-
-
- {' '}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
}
tabs={[
{
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx
new file mode 100644
index 0000000000000..e6dce1bc51367
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx
@@ -0,0 +1,138 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+import { Redirect } from 'react-router-dom';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui';
+
+import { Loading, Error } from '../../../../../components';
+
+import type { PackageInfo } from '../../../../../types';
+import { InstallStatus } from '../../../../../types';
+
+import { useGetPackageInstallStatus, useLink, useStartServices } from '../../../../../hooks';
+
+import type { AssetSavedObject } from './types';
+import { allowedAssetTypes } from './constants';
+import { AssetsAccordion } from './assets_accordion';
+
+interface AssetsPanelProps {
+ packageInfo: PackageInfo;
+}
+
+export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
+ const { name, version } = packageInfo;
+ const {
+ savedObjects: { client: savedObjectsClient },
+ } = useStartServices();
+
+ const { getPath } = useLink();
+ const getPackageInstallStatus = useGetPackageInstallStatus();
+ const packageInstallStatus = getPackageInstallStatus(packageInfo.name);
+
+ const [assetSavedObjects, setAssetsSavedObjects] = useState();
+ const [fetchError, setFetchError] = useState();
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchAssetSavedObjects = async () => {
+ if ('savedObject' in packageInfo) {
+ const {
+ savedObject: { attributes: packageAttributes },
+ } = packageInfo;
+
+ if (
+ !packageAttributes.installed_kibana ||
+ packageAttributes.installed_kibana.length === 0
+ ) {
+ setIsLoading(false);
+ return;
+ }
+
+ try {
+ const objectsToGet = packageAttributes.installed_kibana.map(({ id, type }) => ({
+ id,
+ type,
+ }));
+ const { savedObjects } = await savedObjectsClient.bulkGet(objectsToGet);
+ setAssetsSavedObjects(savedObjects as AssetSavedObject[]);
+ } catch (e) {
+ setFetchError(e);
+ } finally {
+ setIsLoading(false);
+ }
+ } else {
+ setIsLoading(false);
+ }
+ };
+ fetchAssetSavedObjects();
+ }, [savedObjectsClient, packageInfo]);
+
+ // if they arrive at this page and the package is not installed, send them to overview
+ // this happens if they arrive with a direct url or they uninstall while on this tab
+ if (packageInstallStatus.status !== InstallStatus.installed) {
+ return (
+
+ );
+ }
+
+ let content: JSX.Element | Array;
+
+ if (isLoading) {
+ content = ;
+ } else if (fetchError) {
+ content = (
+
+ }
+ error={fetchError}
+ />
+ );
+ } else if (assetSavedObjects === undefined) {
+ content = (
+
+
+
+
+
+ );
+ } else {
+ content = allowedAssetTypes.map((assetType) => {
+ const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType);
+
+ if (!sectionAssetSavedObjects.length) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+ });
+ }
+
+ return (
+
+
+ {content}
+
+ );
+};
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx
new file mode 100644
index 0000000000000..abfdd88d27162
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx
@@ -0,0 +1,92 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import type { FunctionComponent } from 'react';
+
+import {
+ EuiAccordion,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSplitPanel,
+ EuiSpacer,
+ EuiText,
+ EuiLink,
+ EuiHorizontalRule,
+ EuiNotificationBadge,
+} from '@elastic/eui';
+
+import { AssetTitleMap } from '../../../../../constants';
+
+import { getHrefToObjectInKibanaApp, useStartServices } from '../../../../../hooks';
+
+import type { AllowedAssetType, AssetSavedObject } from './types';
+
+interface Props {
+ type: AllowedAssetType;
+ savedObjects: AssetSavedObject[];
+}
+
+export const AssetsAccordion: FunctionComponent = ({ savedObjects, type }) => {
+ const { http } = useStartServices();
+ return (
+
+
+
+ {AssetTitleMap[type]}
+
+
+
+
+ {savedObjects.length}
+
+
+
+ }
+ id={type}
+ >
+ <>
+
+
+ {savedObjects.map(({ id, attributes: { title, description } }, idx) => {
+ const pathToObjectInApp = getHrefToObjectInKibanaApp({
+ http,
+ id,
+ type,
+ });
+ return (
+ <>
+
+
+
+ {pathToObjectInApp ? (
+ {title}
+ ) : (
+ title
+ )}
+
+
+ {description && (
+ <>
+
+
+ {description}
+
+ >
+ )}
+
+ {idx + 1 < savedObjects.length && }
+ >
+ );
+ })}
+
+ >
+
+ );
+};
diff --git a/x-pack/plugins/ml/common/constants/embeddable_map.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts
similarity index 50%
rename from x-pack/plugins/ml/common/constants/embeddable_map.ts
rename to x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts
index 6cb345bae630e..d6d88f7935eb4 100644
--- a/x-pack/plugins/ml/common/constants/embeddable_map.ts
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts
@@ -5,9 +5,12 @@
* 2.0.
*/
-export const COMMON_EMS_LAYER_IDS = [
- 'world_countries',
- 'administrative_regions_lvl2',
- 'usa_zip_codes',
- 'usa_states',
+import { KibanaAssetType } from '../../../../../types';
+
+import type { AllowedAssetTypes } from './types';
+
+export const allowedAssetTypes: AllowedAssetTypes = [
+ KibanaAssetType.dashboard,
+ KibanaAssetType.search,
+ KibanaAssetType.visualization,
];
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts
new file mode 100644
index 0000000000000..ceb030b7ce02e
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+export { AssetsPage } from './assets';
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts
new file mode 100644
index 0000000000000..21efd1cd562e8
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.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 type { SimpleSavedObject } from 'src/core/public';
+
+import type { KibanaAssetType } from '../../../../../types';
+
+export type AssetSavedObject = SimpleSavedObject<{ title: string; description?: string }>;
+
+export type AllowedAssetTypes = [
+ KibanaAssetType.dashboard,
+ KibanaAssetType.search,
+ KibanaAssetType.visualization
+];
+
+export type AllowedAssetType = AllowedAssetTypes[number];
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
index 99a29a8194f9b..cf6007026afeb 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
@@ -56,6 +56,7 @@ import { WithHeaderLayout } from '../../../../layouts';
import { RELEASE_BADGE_DESCRIPTION, RELEASE_BADGE_LABEL } from '../../components/release_badge';
import { IntegrationAgentPolicyCount, UpdateIcon, IconPanel, LoadingIconPanel } from './components';
+import { AssetsPage } from './assets';
import { OverviewPage } from './overview';
import { PackagePoliciesPage } from './policies';
import { SettingsPage } from './settings';
@@ -408,6 +409,24 @@ export function Detail() {
});
}
+ if (packageInstallStatus === InstallStatus.installed && packageInfo.assets) {
+ tabs.push({
+ id: 'assets',
+ name: (
+
+ ),
+ isSelected: panel === 'assets',
+ 'data-test-subj': `tab-assets`,
+ href: getHref('integration_details_assets', {
+ pkgkey: packageInfoKey,
+ ...(integration ? { integration } : {}),
+ }),
+ });
+ }
+
tabs.push({
id: 'settings',
name: (
@@ -476,6 +495,9 @@ export function Detail() {
+
+
+
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
index 7da7328fdebbc..c672abeb1c903 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
@@ -4,9 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+import { stringify, parse } from 'query-string';
import React, { memo, useCallback, useMemo, useState } from 'react';
-import { Redirect } from 'react-router-dom';
+import { Redirect, useLocation, useHistory } from 'react-router-dom';
import type { CriteriaWithPagination, EuiTableFieldDataColumnType } from '@elastic/eui';
import {
EuiButtonIcon,
@@ -15,6 +15,9 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
+ EuiText,
+ EuiButton,
+ EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react';
@@ -66,8 +69,16 @@ interface PackagePoliciesPanelProps {
version: string;
}
export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => {
- const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState(null);
- const { getPath } = useLink();
+ const { search } = useLocation();
+ const history = useHistory();
+ const queryParams = useMemo(() => new URLSearchParams(search), [search]);
+ const agentPolicyIdFromParams = useMemo(() => queryParams.get('addAgentToPolicyId'), [
+ queryParams,
+ ]);
+ const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState(
+ agentPolicyIdFromParams
+ );
+ const { getPath, getHref } = useLink();
const getPackageInstallStatus = useGetPackageInstallStatus();
const packageInstallStatus = getPackageInstallStatus(name);
const { pagination, pageSizeOptions, setPagination } = useUrlPagination();
@@ -87,6 +98,36 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
[setPagination]
);
+ const renderViewDataStepContent = useCallback(
+ () => (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.fleet.epm.agentEnrollment.viewDataDescription.pleaseNoteLabel',
+ { defaultMessage: 'Please note' }
+ )}
+
+ ),
+ }}
+ />
+
+
+
+ {i18n.translate('xpack.fleet.epm.agentEnrollment.viewDataAssetsLabel', {
+ defaultMessage: 'View assets',
+ })}
+
+ >
+ ),
+ [name, version, getHref]
+ );
+
const columns: Array> = useMemo(
() => [
{
@@ -186,12 +227,16 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
align: 'right',
render({ agentPolicy, packagePolicy }) {
return (
-
+
);
},
},
],
- []
+ [renderViewDataStepContent]
);
const noItemsMessage = useMemo(() => {
@@ -236,14 +281,18 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
/>
- {flyoutOpenForPolicyId && (
+ {flyoutOpenForPolicyId && !isLoading && (
setFlyoutOpenForPolicyId(null)}
- agentPolicies={
- data?.items
- .filter(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId)
- .map(({ agentPolicy }) => agentPolicy) ?? []
+ onClose={() => {
+ setFlyoutOpenForPolicyId(null);
+ const { addAgentToPolicyId, ...rest } = parse(search);
+ history.replace({ search: stringify(rest) });
+ }}
+ agentPolicy={
+ data?.items.find(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId)
+ ?.agentPolicy
}
+ viewDataStepContent={renderViewDataStepContent()}
/>
)}
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx
index 995423ea91f96..9e8d200344b01 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx
@@ -233,7 +233,7 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => {
,
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx
deleted file mode 100644
index 55d058a2d7900..0000000000000
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { memo } from 'react';
-import { i18n } from '@kbn/i18n';
-import styled from 'styled-components';
-import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-import { useLinks, useStartServices } from '../../../../hooks';
-
-export const HeroCopy = memo(() => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-
-const Illustration = styled(EuiImage)`
- margin-bottom: -68px;
- width: 80%;
-`;
-
-export const HeroImage = memo(() => {
- const { toAssets } = useLinks();
- const { uiSettings } = useStartServices();
- const IS_DARK_THEME = uiSettings.get('theme:darkMode');
-
- return (
-
- );
-});
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts
index f1055e7e2583e..fcf1078566498 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts
@@ -37,6 +37,7 @@ jest.mock('./steps', () => {
...module,
AgentPolicySelectionStep: jest.fn(),
AgentEnrollmentKeySelectionStep: jest.fn(),
+ ViewDataStep: jest.fn(),
};
});
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx
index db9245b11b0f9..65118044e98c5 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx
@@ -21,7 +21,7 @@ import { FleetStatusProvider, ConfigContext } from '../../hooks';
import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page';
-import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep } from './steps';
+import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep, ViewDataStep } from './steps';
import type { Props } from '.';
import { AgentEnrollmentFlyout } from '.';
@@ -128,6 +128,46 @@ describe(' ', () => {
expect(AgentEnrollmentKeySelectionStep).toHaveBeenCalled();
});
});
+
+ describe('"View data" extension point', () => {
+ it('calls the "View data" step when UI extension is provided', async () => {
+ jest.clearAllMocks();
+ await act(async () => {
+ testBed = await setup({
+ agentPolicies: [],
+ onClose: jest.fn(),
+ viewDataStepContent:
,
+ });
+ testBed.component.update();
+ });
+ const { exists, actions } = testBed;
+ expect(exists('agentEnrollmentFlyout')).toBe(true);
+ expect(ViewDataStep).toHaveBeenCalled();
+
+ jest.clearAllMocks();
+ actions.goToStandaloneTab();
+ expect(ViewDataStep).not.toHaveBeenCalled();
+ });
+
+ it('does not call the "View data" step when UI extension is not provided', async () => {
+ jest.clearAllMocks();
+ await act(async () => {
+ testBed = await setup({
+ agentPolicies: [],
+ onClose: jest.fn(),
+ viewDataStepContent: undefined,
+ });
+ testBed.component.update();
+ });
+ const { exists, actions } = testBed;
+ expect(exists('agentEnrollmentFlyout')).toBe(true);
+ expect(ViewDataStep).not.toHaveBeenCalled();
+
+ jest.clearAllMocks();
+ actions.goToStandaloneTab();
+ expect(ViewDataStep).not.toHaveBeenCalled();
+ });
+ });
});
describe('standalone instructions', () => {
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx
index b91af80691033..58362d85e2fb3 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx
@@ -42,6 +42,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({
onClose,
agentPolicy,
agentPolicies,
+ viewDataStepContent,
}) => {
const [mode, setMode] = useState<'managed' | 'standalone'>('managed');
@@ -109,9 +110,17 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({
}
>
{fleetServerHosts.length === 0 && mode === 'managed' ? null : mode === 'managed' ? (
-
+
) : (
-
+
)}
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx
index e7045173f1257..919f0c3052db9 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx
@@ -21,7 +21,12 @@ import {
useFleetServerInstructions,
} from '../../applications/fleet/sections/agents/agent_requirements_page';
-import { DownloadStep, AgentPolicySelectionStep, AgentEnrollmentKeySelectionStep } from './steps';
+import {
+ DownloadStep,
+ AgentPolicySelectionStep,
+ AgentEnrollmentKeySelectionStep,
+ ViewDataStep,
+} from './steps';
import type { BaseProps } from './types';
type Props = BaseProps;
@@ -53,83 +58,91 @@ const FleetServerMissingRequirements = () => {
return ;
};
-export const ManagedInstructions = React.memo(({ agentPolicy, agentPolicies }) => {
- const fleetStatus = useFleetStatus();
+export const ManagedInstructions = React.memo(
+ ({ agentPolicy, agentPolicies, viewDataStepContent }) => {
+ const fleetStatus = useFleetStatus();
- const [selectedAPIKeyId, setSelectedAPIKeyId] = useState();
- const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false);
+ const [selectedAPIKeyId, setSelectedAPIKeyId] = useState();
+ const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false);
- const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId);
- const settings = useGetSettings();
- const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id);
+ const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId);
+ const settings = useGetSettings();
+ const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id);
- const steps = useMemo(() => {
- const {
- serviceToken,
- getServiceToken,
- isLoadingServiceToken,
- installCommand,
- platform,
- setPlatform,
- } = fleetServerInstructions;
- const fleetServerHosts = settings.data?.item?.fleet_server_hosts || [];
- const baseSteps: EuiContainedStepProps[] = [
- DownloadStep(),
- !agentPolicy
- ? AgentPolicySelectionStep({
- agentPolicies,
- setSelectedAPIKeyId,
- setIsFleetServerPolicySelected,
- })
- : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }),
- ];
- if (isFleetServerPolicySelected) {
- baseSteps.push(
- ...[
- ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }),
- FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }),
- ]
- );
- } else {
- baseSteps.push({
- title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', {
- defaultMessage: 'Enroll and start the Elastic Agent',
- }),
- children: selectedAPIKeyId && apiKey.data && (
-
- ),
- });
- }
- return baseSteps;
- }, [
- agentPolicy,
- agentPolicies,
- selectedAPIKeyId,
- apiKey.data,
- isFleetServerPolicySelected,
- settings.data?.item?.fleet_server_hosts,
- fleetServerInstructions,
- ]);
+ const steps = useMemo(() => {
+ const {
+ serviceToken,
+ getServiceToken,
+ isLoadingServiceToken,
+ installCommand,
+ platform,
+ setPlatform,
+ } = fleetServerInstructions;
+ const fleetServerHosts = settings.data?.item?.fleet_server_hosts || [];
+ const baseSteps: EuiContainedStepProps[] = [
+ DownloadStep(),
+ !agentPolicy
+ ? AgentPolicySelectionStep({
+ agentPolicies,
+ setSelectedAPIKeyId,
+ setIsFleetServerPolicySelected,
+ })
+ : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }),
+ ];
+ if (isFleetServerPolicySelected) {
+ baseSteps.push(
+ ...[
+ ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }),
+ FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }),
+ ]
+ );
+ } else {
+ baseSteps.push({
+ title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', {
+ defaultMessage: 'Enroll and start the Elastic Agent',
+ }),
+ children: selectedAPIKeyId && apiKey.data && (
+
+ ),
+ });
+ }
- return (
- <>
- {fleetStatus.isReady ? (
- <>
-
-
-
-
-
- >
- ) : fleetStatus.missingRequirements?.length === 1 &&
- fleetStatus.missingRequirements[0] === 'fleet_server' ? (
-
- ) : (
-
- )}
- >
- );
-});
+ if (viewDataStepContent) {
+ baseSteps.push(ViewDataStep(viewDataStepContent));
+ }
+
+ return baseSteps;
+ }, [
+ agentPolicy,
+ agentPolicies,
+ selectedAPIKeyId,
+ apiKey.data,
+ isFleetServerPolicySelected,
+ settings.data?.item?.fleet_server_hosts,
+ fleetServerInstructions,
+ viewDataStepContent,
+ ]);
+
+ return (
+ <>
+ {fleetStatus.isReady ? (
+ <>
+
+
+
+
+
+ >
+ ) : fleetStatus.missingRequirements?.length === 1 &&
+ fleetStatus.missingRequirements[0] === 'fleet_server' ? (
+
+ ) : (
+
+ )}
+ >
+ );
+ }
+);
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx
index 636032552a1ae..169ff86b40c88 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx
@@ -10,10 +10,12 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCallOut, EuiLink, EuiButton, EuiSpacer } from '@elastic/eui';
-import { useUrlModal } from '../../hooks';
+import { useUrlModal, useStartServices } from '../../hooks';
export const MissingFleetServerHostCallout: React.FunctionComponent = () => {
const { setModal } = useUrlModal();
+ const { docLinks } = useStartServices();
+
return (
{
defaultMessage="A URL for your Fleet Server host is required to enroll agents with Fleet. You can add this information in Fleet Settings. For more information, see the {link}."
values={{
link: (
-
+
{
+ return {
+ title: i18n.translate('xpack.fleet.agentEnrollment.stepViewDataTitle', {
+ defaultMessage: 'View your data',
+ }),
+ children: content,
+ };
+};
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts
index b9bcf8fb3e4b2..e0c5b040a61fb 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts
@@ -9,12 +9,20 @@ import type { AgentPolicy } from '../../types';
export interface BaseProps {
/**
- * The user selected policy to be used
+ * The user selected policy to be used. If this value is `undefined` a value must be provided for `agentPolicies`.
*/
agentPolicy?: AgentPolicy;
/**
- * A selection of policies for the user to choose from, will be ignored if `agentPolicy` has been provided
+ * A selection of policies for the user to choose from, will be ignored if `agentPolicy` has been provided.
+ *
+ * If this value is `undefined` a value must be provided for `agentPolicy`.
*/
agentPolicies?: AgentPolicy[];
+
+ /**
+ * There is a step in the agent enrollment process that allows users to see the data from an integration represented in the UI
+ * in some way. This is an area for consumers to render a button and text explaining how data can be viewed.
+ */
+ viewDataStepContent?: JSX.Element;
}
diff --git a/x-pack/plugins/fleet/public/components/alpha_flyout.tsx b/x-pack/plugins/fleet/public/components/alpha_flyout.tsx
index c91d80124dd35..b8606968c22dd 100644
--- a/x-pack/plugins/fleet/public/components/alpha_flyout.tsx
+++ b/x-pack/plugins/fleet/public/components/alpha_flyout.tsx
@@ -18,11 +18,15 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { useStartServices } from '../hooks';
+
interface Props {
onClose: () => void;
}
export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => {
+ const { docLinks } = useStartServices();
+
return (
@@ -49,11 +53,7 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => {
defaultMessage="Read our {docsLink} or go to our {forumLink} for questions or feedback."
values={{
docsLink: (
-
+
= ({
fleetServerHosts,
}) => {
const { platform, setPlatform } = usePlatform();
+ const { docLinks } = useStartServices();
const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts);
@@ -85,11 +86,7 @@ export const ManualInstructions: React.FunctionComponent = ({
defaultMessage="See the {link} for RPM / DEB deploy instructions."
values={{
link: (
-
+
= ({
defaultMessage="If you are having trouble connecting, see our {link}."
values={{
link: (
-
+
= ({ agentPolicy, packagePolicy }) => {
+ viewDataStepContent?: JSX.Element;
+}> = ({ agentPolicy, packagePolicy, viewDataStepContent }) => {
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false);
const { getHref } = useLink();
const hasWriteCapabilities = useCapabilities().write;
@@ -103,7 +104,11 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
<>
{isEnrollmentFlyoutOpen && (
-
+
)}
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 6e09310823b32..9bc1bc977b786 100644
--- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
+++ b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
@@ -38,7 +38,7 @@ import {
useGetOutputs,
sendPutOutput,
} from '../../hooks';
-import { isDiffPathProtocol } from '../../../common';
+import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common';
import { SettingsConfirmModal } from './confirm_modal';
import type { SettingsConfirmModalProps } from './confirm_modal';
@@ -53,8 +53,20 @@ interface Props {
onClose: () => void;
}
-function isSameArrayValue(arrayA: string[] = [], arrayB: string[] = []) {
- return arrayA.length === arrayB.length && arrayA.every((val, index) => val === arrayB[index]);
+function normalizeHosts(hostsInput: string[]) {
+ return hostsInput.map((host) => {
+ try {
+ return normalizeHostsForAgents(host);
+ } catch (err) {
+ return host;
+ }
+ });
+}
+
+function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: string[] = []) {
+ const hostsA = normalizeHosts(arrayA);
+ const hostsB = normalizeHosts(arrayB);
+ return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]);
}
function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
@@ -189,6 +201,8 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
}
export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
+ const { docLinks } = useStartServices();
+
const settingsRequest = useGetSettings();
const settings = settingsRequest?.data?.item;
const outputsRequest = useGetOutputs();
@@ -232,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]);
@@ -244,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),
}
);
}
@@ -298,11 +320,11 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
helpText={
@@ -325,7 +347,8 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
defaultMessage: 'Elasticsearch hosts',
})}
helpText={i18n.translate('xpack.fleet.settings.elasticsearchUrlsHelpTect', {
- defaultMessage: 'Specify the Elasticsearch URLs where agents send data.',
+ defaultMessage:
+ 'Specify the Elasticsearch URLs where agents send data. Elasticsearch uses port 9200 by default.',
})}
/>
diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts
index 326cfd804bd57..1688a396cd5a1 100644
--- a/x-pack/plugins/fleet/public/constants/page_paths.ts
+++ b/x-pack/plugins/fleet/public/constants/page_paths.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { stringify } from 'query-string';
+
export type StaticPage =
| 'base'
| 'overview'
@@ -19,6 +21,7 @@ export type StaticPage =
export type DynamicPage =
| 'integration_details_overview'
| 'integration_details_policies'
+ | 'integration_details_assets'
| 'integration_details_settings'
| 'integration_details_custom'
| 'integration_policy_edit'
@@ -66,6 +69,7 @@ export const INTEGRATIONS_ROUTING_PATHS = {
integration_details: '/detail/:pkgkey/:panel?',
integration_details_overview: '/detail/:pkgkey/overview',
integration_details_policies: '/detail/:pkgkey/policies',
+ integration_details_assets: '/detail/:pkgkey/assets',
integration_details_settings: '/detail/:pkgkey/settings',
integration_details_custom: '/detail/:pkgkey/custom',
integration_policy_edit: '/edit-integration/:packagePolicyId',
@@ -86,9 +90,13 @@ export const pagePathGetters: {
INTEGRATIONS_BASE_PATH,
`/detail/${pkgkey}/overview${integration ? `?integration=${integration}` : ''}`,
],
- integration_details_policies: ({ pkgkey, integration }) => [
+ integration_details_policies: ({ pkgkey, integration, addAgentToPolicyId }) => {
+ const qs = stringify({ integration, addAgentToPolicyId });
+ return [INTEGRATIONS_BASE_PATH, `/detail/${pkgkey}/policies${qs ? `?${qs}` : ''}`];
+ },
+ integration_details_assets: ({ pkgkey, integration }) => [
INTEGRATIONS_BASE_PATH,
- `/detail/${pkgkey}/policies${integration ? `?integration=${integration}` : ''}`,
+ `/detail/${pkgkey}/assets${integration ? `?integration=${integration}` : ''}`,
],
integration_details_settings: ({ pkgkey, integration }) => [
INTEGRATIONS_BASE_PATH,
@@ -108,6 +116,7 @@ export const pagePathGetters: {
FLEET_BASE_PATH,
`/policies/${policyId}${tabId ? `/${tabId}` : ''}`,
],
+ // TODO: This might need to be removed because we do not have a way to pick an integration in line anymore
add_integration_from_policy: ({ policyId }) => [
FLEET_BASE_PATH,
`/policies/${policyId}/add-integration`,
diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts
index 9f41e5c7cc92b..a00c0c5dacf11 100644
--- a/x-pack/plugins/fleet/public/hooks/index.ts
+++ b/x-pack/plugins/fleet/public/hooks/index.ts
@@ -11,7 +11,7 @@ export { useConfig, ConfigContext } from './use_config';
export { useKibanaVersion, KibanaVersionContext } from './use_kibana_version';
export { licenseService, useLicense } from './use_license';
export { useLink } from './use_link';
-export { useKibanaLink } from './use_kibana_link';
+export { useKibanaLink, getHrefToObjectInKibanaApp } from './use_kibana_link';
export { usePackageIconType, UsePackageIconType } from './use_package_icon_type';
export { usePagination, Pagination, PAGE_SIZE_OPTIONS } from './use_pagination';
export { useUrlPagination } from './use_url_pagination';
diff --git a/x-pack/plugins/fleet/public/hooks/use_core.ts b/x-pack/plugins/fleet/public/hooks/use_core.ts
index be4a21a094bd4..2c817bfc938f8 100644
--- a/x-pack/plugins/fleet/public/hooks/use_core.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_core.ts
@@ -13,5 +13,6 @@ export function useStartServices(): FleetStartServices {
if (services === null) {
throw new Error('KibanaContextProvider not initialized');
}
+
return services;
}
diff --git a/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts b/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts
index 29f4f8748d1a0..3ad01620b9780 100644
--- a/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts
@@ -4,12 +4,62 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import type { HttpStart } from 'src/core/public';
+
+import { KibanaAssetType } from '../types';
import { useStartServices } from './';
const KIBANA_BASE_PATH = '/app/kibana';
+const getKibanaLink = (http: HttpStart, path: string) => {
+ return http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`);
+};
+
+/**
+ * TODO: This is a temporary solution for getting links to various assets. It is very risky because:
+ *
+ * 1. The plugin might not exist/be enabled
+ * 2. URLs and paths might not always be supported
+ *
+ * We should migrate to using the new URL service locators.
+ *
+ * @deprecated {@link Locators} from the new URL service need to be used instead.
+
+ */
+export const getHrefToObjectInKibanaApp = ({
+ type,
+ id,
+ http,
+}: {
+ type: KibanaAssetType;
+ id: string;
+ http: HttpStart;
+}): undefined | string => {
+ let kibanaAppPath: undefined | string;
+ switch (type) {
+ case KibanaAssetType.dashboard:
+ kibanaAppPath = `/dashboard/${id}`;
+ break;
+ case KibanaAssetType.search:
+ kibanaAppPath = `/discover/${id}`;
+ break;
+ case KibanaAssetType.visualization:
+ kibanaAppPath = `/visualize/edit/${id}`;
+ break;
+ default:
+ return undefined;
+ }
+
+ return getKibanaLink(http, kibanaAppPath);
+};
+
+/**
+ * TODO: This functionality needs to be replaced with use of the new URL service locators
+ *
+ * @deprecated {@link Locators} from the new URL service need to be used instead.
+ */
export function useKibanaLink(path: string = '/') {
- const core = useStartServices();
- return core.http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`);
+ const { http } = useStartServices();
+ return getKibanaLink(http, path);
}
diff --git a/x-pack/plugins/fleet/public/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/mock/plugin_configuration.ts
index 097b6aa98c067..5dad8ad504979 100644
--- a/x-pack/plugins/fleet/public/mock/plugin_configuration.ts
+++ b/x-pack/plugins/fleet/public/mock/plugin_configuration.ts
@@ -12,6 +12,7 @@ export const createConfigurationMock = (): FleetConfigType => {
enabled: true,
registryUrl: '',
registryProxyUrl: '',
+ agentIdVerificationEnabled: true,
agents: {
enabled: true,
elasticsearch: {
diff --git a/x-pack/plugins/fleet/public/search_provider.test.ts b/x-pack/plugins/fleet/public/search_provider.test.ts
index fa2e8508c938e..c42ac2e5a1551 100644
--- a/x-pack/plugins/fleet/public/search_provider.test.ts
+++ b/x-pack/plugins/fleet/public/search_provider.test.ts
@@ -185,5 +185,105 @@ describe('Package search provider', () => {
expect(sendGetPackages).toHaveBeenCalledTimes(1);
});
+
+ describe('tags', () => {
+ test('without packages tag, without search term', () => {
+ getTestScheduler().run(({ hot, expectObservable }) => {
+ mockSendGetPackages.mockReturnValue(
+ hot('--(a|)', { a: { data: { response: testResponse } } })
+ );
+ setupMock.getStartServices.mockReturnValue(
+ hot('--(a|)', { a: [coreMock.createStart()] }) as any
+ );
+ const packageSearchProvider = createPackageSearchProvider(setupMock);
+ expectObservable(
+ packageSearchProvider.find(
+ { types: ['test'] },
+ { aborted$: NEVER, maxResults: 100, preference: '' }
+ )
+ ).toBe('(a|)', {
+ a: [],
+ });
+ });
+
+ expect(sendGetPackages).toHaveBeenCalledTimes(0);
+ });
+
+ test('with packages tag, with no search term', () => {
+ getTestScheduler().run(({ hot, expectObservable }) => {
+ mockSendGetPackages.mockReturnValue(
+ hot('--(a|)', { a: { data: { response: testResponse } } })
+ );
+ setupMock.getStartServices.mockReturnValue(
+ hot('--(a|)', { a: [coreMock.createStart()] }) as any
+ );
+ const packageSearchProvider = createPackageSearchProvider(setupMock);
+ expectObservable(
+ packageSearchProvider.find(
+ { types: ['package'] },
+ { aborted$: NEVER, maxResults: 100, preference: '' }
+ )
+ ).toBe('--(a|)', {
+ a: [
+ {
+ id: 'test-test',
+ score: 80,
+ title: 'test',
+ type: 'package',
+ url: {
+ path: 'undefined#/detail/test-test/overview',
+ prependBasePath: false,
+ },
+ },
+ {
+ id: 'test1-test1',
+ score: 80,
+ title: 'test1',
+ type: 'package',
+ url: {
+ path: 'undefined#/detail/test1-test1/overview',
+ prependBasePath: false,
+ },
+ },
+ ],
+ });
+ });
+
+ expect(sendGetPackages).toHaveBeenCalledTimes(1);
+ });
+
+ test('with packages tag, with search term', () => {
+ getTestScheduler().run(({ hot, expectObservable }) => {
+ mockSendGetPackages.mockReturnValue(
+ hot('--(a|)', { a: { data: { response: testResponse } } })
+ );
+ setupMock.getStartServices.mockReturnValue(
+ hot('--(a|)', { a: [coreMock.createStart()] }) as any
+ );
+ const packageSearchProvider = createPackageSearchProvider(setupMock);
+ expectObservable(
+ packageSearchProvider.find(
+ { term: 'test1', types: ['package'] },
+ { aborted$: NEVER, maxResults: 100, preference: '' }
+ )
+ ).toBe('--(a|)', {
+ a: [
+ {
+ id: 'test1-test1',
+ score: 80,
+ title: 'test1',
+ type: 'package',
+ url: {
+ path: 'undefined#/detail/test1-test1/overview',
+ prependBasePath: false,
+ },
+ },
+ ],
+ });
+ });
+
+ expect(sendGetPackages).toHaveBeenCalledTimes(1);
+ });
+ });
});
});
diff --git a/x-pack/plugins/fleet/public/search_provider.ts b/x-pack/plugins/fleet/public/search_provider.ts
index cd4ec1c29b457..56e08ecad29fb 100644
--- a/x-pack/plugins/fleet/public/search_provider.ts
+++ b/x-pack/plugins/fleet/public/search_provider.ts
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import type { CoreSetup, CoreStart } from 'src/core/public';
+import type { CoreSetup, CoreStart, ApplicationStart } from 'src/core/public';
import type { Observable } from 'rxjs';
import { from, of, combineLatest } from 'rxjs';
@@ -34,6 +34,26 @@ const createPackages$ = () =>
shareReplay(1)
);
+const toSearchResult = (
+ pkg: GetPackagesResponse['response'][number],
+ application: ApplicationStart
+) => {
+ const pkgkey = `${pkg.name}-${pkg.version}`;
+ return {
+ id: pkgkey,
+ type: packageType,
+ title: pkg.title,
+ score: 80,
+ url: {
+ // TODO: See https://github.com/elastic/kibana/issues/96134 for details about why we use '#' here. Below should be updated
+ // as part of migrating to non-hash based router.
+ // prettier-ignore
+ path: `${application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}#${pagePathGetters.integration_details_overview({ pkgkey })[1]}`,
+ prependBasePath: false,
+ },
+ };
+};
+
export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResultProvider => {
const coreStart$ = from(core.getStartServices()).pipe(
map(([coreStart]) => coreStart),
@@ -52,12 +72,23 @@ export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResult
return {
id: 'packages',
getSearchableTypes: () => [packageType],
- find: ({ term }, { maxResults, aborted$ }) => {
- if (!term) {
+ find: ({ term, types }, { maxResults, aborted$ }) => {
+ if (types?.includes(packageType) === false) {
return of([]);
}
- term = term.toLowerCase();
+ const hasTypes = Boolean(types);
+ const typesIncludePackage = hasTypes && types!.includes(packageType);
+ const noSearchTerm = !term;
+ const includeAllPackages = typesIncludePackage && noSearchTerm;
+
+ if (!includeAllPackages && noSearchTerm) {
+ return of([]);
+ }
+
+ if (term) {
+ term = term.toLowerCase();
+ }
const toSearchResults = (
coreStart: CoreStart,
@@ -65,25 +96,17 @@ export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResult
): GlobalSearchProviderResult[] => {
const packages = packagesResponse.slice(0, maxResults);
- return packages.flatMap((pkg) => {
- if (!term || !pkg.title.toLowerCase().includes(term)) {
- return [];
- }
- const pkgkey = `${pkg.name}-${pkg.version}`;
- return {
- id: pkgkey,
- type: packageType,
- title: pkg.title,
- score: 80,
- url: {
- // TODO: See https://github.com/elastic/kibana/issues/96134 for details about why we use '#' here. Below should be updated
- // as part of migrating to non-hash based router.
- // prettier-ignore
- path: `${coreStart.application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}#${pagePathGetters.integration_details_overview({ pkgkey })[1]}`,
- prependBasePath: false,
- },
- };
- });
+ return packages.flatMap(
+ includeAllPackages
+ ? (pkg) => toSearchResult(pkg, coreStart.application)
+ : (pkg) => {
+ if (!term || !pkg.title.toLowerCase().includes(term)) {
+ return [];
+ }
+
+ return toSearchResult(pkg, coreStart.application);
+ }
+ );
};
return combineLatest([coreStart$, getPackages$()]).pipe(
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts
similarity index 64%
rename from x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts
rename to x-pack/plugins/fleet/server/constants/fleet_es_assets.ts
index 4c0484c058abf..8e9dac11db799 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts
+++ b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts
@@ -5,9 +5,37 @@
* 2.0.
*/
-export const FINAL_PIPELINE_ID = '.fleet_final_pipeline';
+export const FLEET_FINAL_PIPELINE_ID = '.fleet_final_pipeline-1';
-export const FINAL_PIPELINE = `---
+export const FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME = '.fleet_component_template-1';
+
+export const FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT = {
+ _meta: {},
+ template: {
+ settings: {
+ index: {
+ final_pipeline: FLEET_FINAL_PIPELINE_ID,
+ },
+ },
+ mappings: {
+ properties: {
+ event: {
+ properties: {
+ ingested: {
+ type: 'date',
+ },
+ agent_id_status: {
+ ignore_above: 1024,
+ type: 'keyword',
+ },
+ },
+ },
+ },
+ },
+ },
+};
+
+export const FLEET_FINAL_PIPELINE_CONTENT = `---
description: >
Final pipeline for processing all incoming Fleet Agent documents.
processors:
@@ -59,25 +87,26 @@ processors:
}
String verified(def ctx, def params) {
- // Agents only use API keys.
- if (ctx?._security?.authentication_type == null || ctx._security.authentication_type != 'API_KEY') {
- return "no_api_key";
- }
-
- // Verify the API key owner before trusting any metadata it contains.
- if (!is_user_trusted(ctx, params.trusted_users)) {
- return "untrusted_user";
+ // No agent.id field to validate.
+ if (ctx?.agent?.id == null) {
+ return "missing";
}
- // API keys created by Fleet include metadata about the agent they were issued to.
- if (ctx?._security?.api_key?.metadata?.agent_id == null || ctx?.agent?.id == null) {
- return "missing_metadata";
+ // Check auth metadata from API key.
+ if (ctx?._security?.authentication_type == null
+ // Agents only use API keys.
+ || ctx._security.authentication_type != 'API_KEY'
+ // Verify the API key owner before trusting any metadata it contains.
+ || !is_user_trusted(ctx, params.trusted_users)
+ // Verify the API key has metadata indicating the assigned agent ID.
+ || ctx?._security?.api_key?.metadata?.agent_id == null) {
+ return "auth_metadata_missing";
}
// The API key can only be used represent the agent.id it was issued to.
if (ctx._security.api_key.metadata.agent_id != ctx.agent.id) {
// Potential masquerade attempt.
- return "agent_id_mismatch";
+ return "mismatch";
}
return "verified";
diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts
index 16a92a2ffa1aa..3aca5e8800dc5 100644
--- a/x-pack/plugins/fleet/server/constants/index.ts
+++ b/x-pack/plugins/fleet/server/constants/index.ts
@@ -57,3 +57,10 @@ export {
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
PRECONFIGURATION_LATEST_KEYWORD,
} from '../../common';
+
+export {
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT,
+ FLEET_FINAL_PIPELINE_ID,
+ FLEET_FINAL_PIPELINE_CONTENT,
+} from './fleet_es_assets';
diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts
index 0a886ffedbd6c..ab1cd9002d04a 100644
--- a/x-pack/plugins/fleet/server/index.ts
+++ b/x-pack/plugins/fleet/server/index.ts
@@ -77,6 +77,7 @@ export const config: PluginConfigDescriptor = {
}),
packages: PreconfiguredPackagesSchema,
agentPolicies: PreconfiguredAgentPoliciesSchema,
+ agentIdVerificationEnabled: schema.boolean({ defaultValue: true }),
}),
};
diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts
index a94f274b202ad..43a5a14b425b5 100644
--- a/x-pack/plugins/fleet/server/mocks/index.ts
+++ b/x-pack/plugins/fleet/server/mocks/index.ts
@@ -4,6 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import { of } from 'rxjs';
+
import {
elasticsearchServiceMock,
loggingSystemMock,
@@ -22,6 +24,14 @@ import type { FleetAppContext } from '../plugin';
export * from '../services/artifacts/mocks';
export const createAppContextStartContractMock = (): FleetAppContext => {
+ const config = {
+ agents: { enabled: true, elasticsearch: {} },
+ enabled: true,
+ agentIdVerificationEnabled: true,
+ };
+
+ const config$ = of(config);
+
return {
elasticsearch: elasticsearchServiceMock.createStart(),
data: dataPluginMock.createStartContract(),
@@ -33,7 +43,9 @@ export const createAppContextStartContractMock = (): FleetAppContext => {
configInitialValue: {
agents: { enabled: true, elasticsearch: {} },
enabled: true,
+ agentIdVerificationEnabled: true,
},
+ config$,
kibanaVersion: '8.0.0',
kibanaBranch: 'master',
};
diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts
index b80ddfe8e7c9b..073ff7806d9fe 100644
--- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts
+++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+import type { estypes } from '@elastic/elasticsearch';
import { keyBy, keys, merge } from 'lodash';
import type { RequestHandler, SavedObjectsBulkGetObject } from 'src/core/server';
@@ -140,10 +140,7 @@ export const getListHandler: RequestHandler = async (context, request, response)
// Query backing indices to extract data stream dataset, namespace, and type values
const {
- body: {
- // @ts-expect-error @elastic/elasticsearch aggregations are not typed
- aggregations: { dataset, namespace, type },
- },
+ body: { aggregations: dataStreamAggs },
} = await esClient.search({
index: dataStream.indices.map((index) => index.index_name),
body: {
@@ -187,6 +184,11 @@ export const getListHandler: RequestHandler = async (context, request, response)
},
});
+ const { dataset, namespace, type } = dataStreamAggs as Record<
+ string,
+ estypes.AggregationsMultiBucketAggregate<{ key?: string }>
+ >;
+
// Set values from backing indices query
dataStreamResponse.dataset = dataset.buckets[0]?.key || '';
dataStreamResponse.namespace = namespace.buckets[0]?.key || '';
diff --git a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
index 77fe74fda54d9..d6c483ffe30d9 100644
--- a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
+++ b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
@@ -15,7 +15,7 @@ import { PutPreconfigurationSchema } from '../../types';
import { defaultIngestErrorHandler } from '../../errors';
import { ensurePreconfiguredPackagesAndPolicies, outputService } from '../../services';
-export const putPreconfigurationHandler: RequestHandler<
+export const updatePreconfigurationHandler: RequestHandler<
undefined,
undefined,
TypeOf
@@ -43,10 +43,10 @@ export const putPreconfigurationHandler: RequestHandler<
export const registerRoutes = (router: IRouter) => {
router.put(
{
- path: PRECONFIGURATION_API_ROUTES.PUT_PRECONFIG,
+ path: PRECONFIGURATION_API_ROUTES.UPDATE_PATTERN,
validate: PutPreconfigurationSchema,
options: { tags: [`access:${PLUGIN_ID}-all`] },
},
- putPreconfigurationHandler
+ updatePreconfigurationHandler
);
};
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index bd7bb98eb7c07..fe8771115a217 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -149,6 +149,7 @@ const getSavedObjectTypes = (
is_managed: { type: 'boolean' },
status: { type: 'keyword' },
package_policies: { type: 'keyword' },
+ unenroll_timeout: { type: 'integer' },
updated_at: { type: 'date' },
updated_by: { type: 'keyword' },
revision: { type: 'integer' },
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 2a6036d99281e..465075cca7a0b 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -642,6 +642,7 @@ class AgentPolicyService {
data: (fullPolicy as unknown) as FleetServerPolicy['data'],
policy_id: fullPolicy.id,
default_fleet_server: policy.is_default_fleet_server === true,
+ unenroll_timeout: policy.unenroll_timeout,
};
await esClient.create({
diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
index 8f575f4969cf4..14d43e6e219db 100644
--- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
+++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
@@ -47,7 +47,7 @@ export async function listEnrollmentApiKeys(
body: query ? { query } : undefined,
});
- // @ts-expect-error @elastic/elasticsearch
+ // @ts-expect-error @elastic/elasticsearch _source is optional
const items = res.body.hits.hits.map(esDocToEnrollmentApiKey);
return {
diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts
index 5681be3e8793b..b046b41d73722 100644
--- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts
+++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts
@@ -125,6 +125,7 @@ describe('When using the artifacts services', () => {
expect(esClientMock.delete).toHaveBeenCalledWith({
index: FLEET_SERVER_ARTIFACTS_INDEX,
id: '123',
+ refresh: 'wait_for',
});
});
diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts
index 26032ab94dbc8..6ac23cb1f9ef8 100644
--- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts
+++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts
@@ -87,6 +87,7 @@ export const deleteArtifact = async (esClient: ElasticsearchClient, id: string):
await esClient.delete({
index: FLEET_SERVER_ARTIFACTS_INDEX,
id,
+ refresh: 'wait_for',
});
} catch (e) {
throw new ArtifactsElasticsearchError(e);
diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts
index bc4ffffb68358..1be0f73a347e9 100644
--- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts
@@ -129,6 +129,13 @@ processors:
password: {{password}}
{{#if password}}
hidden_password: {{password}}
+{{/if}}
+ `;
+ const streamTemplateWithString = `
+{{#if (contains ".pcap" file)}}
+pcap: true
+{{else}}
+pcap: false
{{/if}}
`;
@@ -168,6 +175,28 @@ hidden_password: {{password}}
tags: ['foo', 'bar'],
});
});
+
+ it('should support strings', () => {
+ const vars = {
+ file: { value: 'foo.pcap' },
+ };
+
+ const output = compileTemplate(vars, streamTemplateWithString);
+ expect(output).toEqual({
+ pcap: true,
+ });
+ });
+
+ it('should support strings with no match', () => {
+ const vars = {
+ file: { value: 'file' },
+ };
+
+ const output = compileTemplate(vars, streamTemplateWithString);
+ expect(output).toEqual({
+ pcap: false,
+ });
+ });
});
it('should support optional yaml values at root level', () => {
diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts
index 84a8ab581354a..a0d14e6962a8d 100644
--- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts
+++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts
@@ -111,11 +111,12 @@ function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateSt
return { vars, yamlValues };
}
-function containsHelper(this: any, item: string, list: string[], options: any) {
- if (Array.isArray(list) && list.includes(item)) {
+function containsHelper(this: any, item: string, check: string | string[], options: any) {
+ if ((Array.isArray(check) || typeof check === 'string') && check.includes(item)) {
if (options && options.fn) {
return options.fn(this);
}
+ return true;
}
return '';
}
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
index 1d212f188120f..a6aa87c5ed0f5 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
@@ -14,9 +14,9 @@ import { getAsset, getPathParts } from '../../archive';
import type { ArchiveEntry } from '../../archive';
import { saveInstalledEsRefs } from '../../packages/install';
import { getInstallationObject } from '../../packages';
+import { FLEET_FINAL_PIPELINE_CONTENT, FLEET_FINAL_PIPELINE_ID } from '../../../../constants';
import { deletePipelineRefs } from './remove';
-import { FINAL_PIPELINE, FINAL_PIPELINE_ID } from './final_pipeline';
interface RewriteSubstitution {
source: string;
@@ -190,22 +190,24 @@ export async function ensureFleetFinalPipelineIsInstalled(esClient: Elasticsearc
const esClientRequestOptions: TransportRequestOptions = {
ignore: [404],
};
- const res = await esClient.ingest.getPipeline({ id: FINAL_PIPELINE_ID }, esClientRequestOptions);
+ const res = await esClient.ingest.getPipeline(
+ { id: FLEET_FINAL_PIPELINE_ID },
+ esClientRequestOptions
+ );
if (res.statusCode === 404) {
- await esClient.ingest.putPipeline(
- // @ts-ignore pipeline is define in yaml
- { id: FINAL_PIPELINE_ID, body: FINAL_PIPELINE },
- {
- headers: {
- // pipeline is YAML
- 'Content-Type': 'application/yaml',
- // but we want JSON responses (to extract error messages, status code, or other metadata)
- Accept: 'application/json',
- },
- }
- );
+ await installPipeline({
+ esClient,
+ pipeline: {
+ nameForInstallation: FLEET_FINAL_PIPELINE_ID,
+ contentForInstallation: FLEET_FINAL_PIPELINE_CONTENT,
+ extension: 'yml',
+ },
+ });
+ return { isCreated: true };
}
+
+ return { isCreated: false };
}
const isDirectory = ({ path }: ArchiveEntry) => path.endsWith('/');
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
index acf8ae742bf8f..6a4476316bfa5 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
@@ -25,8 +25,7 @@ exports[`EPM template tests loading base.yml: base.yml 1`] = `
"default_field": [
"long.nested.foo"
]
- },
- "final_pipeline": ".fleet_final_pipeline"
+ }
}
},
"mappings": {
@@ -99,7 +98,9 @@ exports[`EPM template tests loading base.yml: base.yml 1`] = `
}
},
"data_stream": {},
- "composed_of": [],
+ "composed_of": [
+ ".fleet_component_template-1"
+ ],
"_meta": {
"package": {
"name": "nginx"
@@ -140,8 +141,7 @@ exports[`EPM template tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
"coredns.response.code",
"coredns.response.flags"
]
- },
- "final_pipeline": ".fleet_final_pipeline"
+ }
}
},
"mappings": {
@@ -214,7 +214,9 @@ exports[`EPM template tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
}
},
"data_stream": {},
- "composed_of": [],
+ "composed_of": [
+ ".fleet_component_template-1"
+ ],
"_meta": {
"package": {
"name": "coredns"
@@ -283,8 +285,7 @@ exports[`EPM template tests loading system.yml: system.yml 1`] = `
"system.users.scope",
"system.users.remote_host"
]
- },
- "final_pipeline": ".fleet_final_pipeline"
+ }
}
},
"mappings": {
@@ -1741,7 +1742,9 @@ exports[`EPM template tests loading system.yml: system.yml 1`] = `
}
},
"data_stream": {},
- "composed_of": [],
+ "composed_of": [
+ ".fleet_component_template-1"
+ ],
"_meta": {
"package": {
"name": "system"
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
index d202dab54f5bd..e8dac60ddba1a 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
@@ -11,7 +11,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/s
import { ElasticsearchAssetType } from '../../../../types';
import type {
RegistryDataStream,
- TemplateRef,
+ IndexTemplateEntry,
RegistryElasticsearch,
InstallablePackage,
} from '../../../../types';
@@ -19,7 +19,11 @@ import { loadFieldsFromYaml, processFields } from '../../fields/field';
import type { Field } from '../../fields/field';
import { getPipelineNameForInstallation } from '../ingest_pipeline/install';
import { getAsset, getPathParts } from '../../archive';
-import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install';
+import { removeAssetTypesFromInstalledEs, saveInstalledEsRefs } from '../../packages/install';
+import {
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT,
+} from '../../../../constants';
import {
generateMappings,
@@ -34,7 +38,7 @@ export const installTemplates = async (
esClient: ElasticsearchClient,
paths: string[],
savedObjectsClient: SavedObjectsClientContract
-): Promise => {
+): Promise => {
// install any pre-built index template assets,
// atm, this is only the base package's global index templates
// Install component templates first, as they are used by the index templates
@@ -42,44 +46,36 @@ export const installTemplates = async (
await installPreBuiltTemplates(paths, esClient);
// remove package installation's references to index templates
- await removeAssetsFromInstalledEsByType(
- savedObjectsClient,
- installablePackage.name,
- ElasticsearchAssetType.indexTemplate
- );
+ await removeAssetTypesFromInstalledEs(savedObjectsClient, installablePackage.name, [
+ ElasticsearchAssetType.indexTemplate,
+ ElasticsearchAssetType.componentTemplate,
+ ]);
// build templates per data stream from yml files
const dataStreams = installablePackage.data_streams;
if (!dataStreams) return [];
+
+ const installedTemplatesNested = await Promise.all(
+ dataStreams.map((dataStream) =>
+ installTemplateForDataStream({
+ pkg: installablePackage,
+ esClient,
+ dataStream,
+ })
+ )
+ );
+ const installedTemplates = installedTemplatesNested.flat();
+
// get template refs to save
- const installedTemplateRefs = dataStreams.map((dataStream) => ({
- id: generateTemplateName(dataStream),
- type: ElasticsearchAssetType.indexTemplate,
- }));
+ const installedIndexTemplateRefs = getAllTemplateRefs(installedTemplates);
// add package installation's references to index templates
- await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, installedTemplateRefs);
-
- if (dataStreams) {
- const installTemplatePromises = dataStreams.reduce>>(
- (acc, dataStream) => {
- acc.push(
- installTemplateForDataStream({
- pkg: installablePackage,
- esClient,
- dataStream,
- })
- );
- return acc;
- },
- []
- );
-
- const res = await Promise.all(installTemplatePromises);
- const installedTemplates = res.flat();
+ await saveInstalledEsRefs(
+ savedObjectsClient,
+ installablePackage.name,
+ installedIndexTemplateRefs
+ );
- return installedTemplates;
- }
- return [];
+ return installedTemplates;
};
const installPreBuiltTemplates = async (paths: string[], esClient: ElasticsearchClient) => {
@@ -160,7 +156,7 @@ export async function installTemplateForDataStream({
pkg: InstallablePackage;
esClient: ElasticsearchClient;
dataStream: RegistryDataStream;
-}): Promise {
+}): Promise {
const fields = await loadFieldsFromYaml(pkg, dataStream.path);
return installTemplate({
esClient,
@@ -171,84 +167,140 @@ export async function installTemplateForDataStream({
});
}
+interface TemplateMapEntry {
+ _meta: { package?: { name: string } };
+ template:
+ | {
+ mappings: NonNullable;
+ }
+ | {
+ settings: NonNullable | object;
+ };
+}
+type TemplateMap = Record;
function putComponentTemplate(
- body: object | undefined,
- name: string,
- esClient: ElasticsearchClient
-): { clusterPromise: Promise; name: string } | undefined {
- if (body) {
- const esClientParams = {
- name,
- body,
- };
-
- return {
- // @ts-expect-error body expected to be ClusterPutComponentTemplateRequest
- clusterPromise: esClient.cluster.putComponentTemplate(esClientParams, { ignore: [404] }),
- name,
- };
+ esClient: ElasticsearchClient,
+ params: {
+ body: TemplateMapEntry;
+ name: string;
+ create?: boolean;
}
+): { clusterPromise: Promise; name: string } {
+ const { name, body, create = false } = params;
+ return {
+ clusterPromise: esClient.cluster.putComponentTemplate(
+ // @ts-expect-error body is missing required key `settings`. TemplateMapEntry has settings *or* mappings
+ { name, body, create },
+ { ignore: [404] }
+ ),
+ name,
+ };
}
-function buildComponentTemplates(registryElasticsearch: RegistryElasticsearch | undefined) {
- let mappingsTemplate;
- let settingsTemplate;
+const mappingsSuffix = '@mappings';
+const settingsSuffix = '@settings';
+const userSettingsSuffix = '@custom';
+type TemplateBaseName = string;
+type UserSettingsTemplateName = `${TemplateBaseName}${typeof userSettingsSuffix}`;
+
+const isUserSettingsTemplate = (name: string): name is UserSettingsTemplateName =>
+ name.endsWith(userSettingsSuffix);
+
+function buildComponentTemplates(params: {
+ templateName: string;
+ registryElasticsearch: RegistryElasticsearch | undefined;
+ packageName: string;
+}) {
+ const { templateName, registryElasticsearch, packageName } = params;
+ const mappingsTemplateName = `${templateName}${mappingsSuffix}`;
+ const settingsTemplateName = `${templateName}${settingsSuffix}`;
+ const userSettingsTemplateName = `${templateName}${userSettingsSuffix}`;
+
+ const templatesMap: TemplateMap = {};
+ const _meta = { package: { name: packageName } };
if (registryElasticsearch && registryElasticsearch['index_template.mappings']) {
- mappingsTemplate = {
+ templatesMap[mappingsTemplateName] = {
template: {
- mappings: {
- ...registryElasticsearch['index_template.mappings'],
- },
+ mappings: registryElasticsearch['index_template.mappings'],
},
+ _meta,
};
}
if (registryElasticsearch && registryElasticsearch['index_template.settings']) {
- settingsTemplate = {
+ templatesMap[settingsTemplateName] = {
template: {
settings: registryElasticsearch['index_template.settings'],
},
+ _meta,
};
}
- return { settingsTemplate, mappingsTemplate };
-}
-async function installDataStreamComponentTemplates(
- templateName: string,
- registryElasticsearch: RegistryElasticsearch | undefined,
- esClient: ElasticsearchClient
-) {
- const templates: string[] = [];
- const componentPromises: Array> = [];
+ // return empty/stub template
+ templatesMap[userSettingsTemplateName] = {
+ template: {
+ settings: {},
+ },
+ _meta,
+ };
- const compTemplates = buildComponentTemplates(registryElasticsearch);
+ return templatesMap;
+}
- const mappings = putComponentTemplate(
- compTemplates.mappingsTemplate,
- `${templateName}-mappings`,
- esClient
- );
+async function installDataStreamComponentTemplates(params: {
+ templateName: string;
+ registryElasticsearch: RegistryElasticsearch | undefined;
+ esClient: ElasticsearchClient;
+ packageName: string;
+}) {
+ const { templateName, registryElasticsearch, esClient, packageName } = params;
+ const templates = buildComponentTemplates({ templateName, registryElasticsearch, packageName });
+ const templateNames = Object.keys(templates);
+ const templateEntries = Object.entries(templates);
- const settings = putComponentTemplate(
- compTemplates.settingsTemplate,
- `${templateName}-settings`,
- esClient
+ // TODO: Check return values for errors
+ await Promise.all(
+ templateEntries.map(async ([name, body]) => {
+ if (isUserSettingsTemplate(name)) {
+ // look for existing user_settings template
+ const result = await esClient.cluster.getComponentTemplate({ name }, { ignore: [404] });
+ const hasUserSettingsTemplate = result.body.component_templates?.length === 1;
+ if (!hasUserSettingsTemplate) {
+ // only add if one isn't already present
+ const { clusterPromise } = putComponentTemplate(esClient, { body, name, create: true });
+ return clusterPromise;
+ }
+ } else {
+ const { clusterPromise } = putComponentTemplate(esClient, { body, name });
+ return clusterPromise;
+ }
+ })
);
- if (mappings) {
- templates.push(mappings.name);
- componentPromises.push(mappings.clusterPromise);
- }
+ return templateNames;
+}
- if (settings) {
- templates.push(settings.name);
- componentPromises.push(settings.clusterPromise);
+export async function ensureDefaultComponentTemplate(esClient: ElasticsearchClient) {
+ const { body: getTemplateRes } = await esClient.cluster.getComponentTemplate(
+ {
+ name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ },
+ {
+ ignore: [404],
+ }
+ );
+
+ const existingTemplate = getTemplateRes?.component_templates?.[0];
+ if (!existingTemplate) {
+ await putComponentTemplate(esClient, {
+ name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ body: FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT,
+ create: true,
+ });
}
- // TODO: Check return values for errors
- await Promise.all(componentPromises);
- return templates;
+ return { isCreated: !existingTemplate };
}
export async function installTemplate({
@@ -263,7 +315,7 @@ export async function installTemplate({
dataStream: RegistryDataStream;
packageVersion: string;
packageName: string;
-}): Promise {
+}): Promise {
const validFields = processFields(fields);
const mappings = generateMappings(validFields);
const templateName = generateTemplateName(dataStream);
@@ -310,11 +362,12 @@ export async function installTemplate({
await esClient.indices.putIndexTemplate(updateIndexTemplateParams, { ignore: [404] });
}
- const composedOfTemplates = await installDataStreamComponentTemplates(
+ const composedOfTemplates = await installDataStreamComponentTemplates({
templateName,
- dataStream.elasticsearch,
- esClient
- );
+ registryElasticsearch: dataStream.elasticsearch,
+ esClient,
+ packageName,
+ });
const template = getTemplate({
type: dataStream.type,
@@ -342,3 +395,22 @@ export async function installTemplate({
indexTemplate: template,
};
}
+
+export function getAllTemplateRefs(installedTemplates: IndexTemplateEntry[]) {
+ return installedTemplates.flatMap((installedTemplate) => {
+ const indexTemplates = [
+ {
+ id: installedTemplate.templateName,
+ type: ElasticsearchAssetType.indexTemplate,
+ },
+ ];
+ const componentTemplates = installedTemplate.indexTemplate.composed_of
+ // Filter global component template shared between integrations
+ .filter((componentTemplateId) => componentTemplateId !== FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME)
+ .map((componentTemplateId) => ({
+ id: componentTemplateId,
+ type: ElasticsearchAssetType.componentTemplate,
+ }));
+ return indexTemplates.concat(componentTemplates);
+ });
+}
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
index ae7bff618dba2..d1f806f67ca5c 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
@@ -24,6 +24,8 @@ import {
generateTemplateIndexPattern,
} from './template';
+const FLEET_COMPONENT_TEMPLATE = '.fleet_component_template-1';
+
// Add our own serialiser to just do JSON.stringify
expect.addSnapshotSerializer({
print(val) {
@@ -67,7 +69,7 @@ describe('EPM template', () => {
composedOfTemplates,
templatePriority: 200,
});
- expect(template.composed_of).toStrictEqual(composedOfTemplates);
+ expect(template.composed_of).toStrictEqual([...composedOfTemplates, FLEET_COMPONENT_TEMPLATE]);
});
it('adds empty composed_of correctly', () => {
@@ -82,7 +84,7 @@ describe('EPM template', () => {
composedOfTemplates,
templatePriority: 200,
});
- expect(template.composed_of).toStrictEqual(composedOfTemplates);
+ expect(template.composed_of).toStrictEqual([FLEET_COMPONENT_TEMPLATE]);
});
it('adds hidden field correctly', () => {
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
index 07d0df021c827..6aa7680395bed 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
@@ -10,13 +10,13 @@ import type { ElasticsearchClient } from 'kibana/server';
import type { Field, Fields } from '../../fields/field';
import type {
RegistryDataStream,
- TemplateRef,
+ IndexTemplateEntry,
IndexTemplate,
IndexTemplateMappings,
} from '../../../../types';
import { appContextService } from '../../../';
import { getRegistryDataStreamAssetBaseName } from '../index';
-import { FINAL_PIPELINE_ID } from '../ingest_pipeline/final_pipeline';
+import { FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME } from '../../../../constants';
interface Properties {
[key: string]: any;
@@ -90,7 +90,11 @@ export function getTemplate({
if (template.template.settings.index.final_pipeline) {
throw new Error(`Error template for ${templateIndexPattern} contains a final_pipeline`);
}
- template.template.settings.index.final_pipeline = FINAL_PIPELINE_ID;
+
+ if (appContextService.getConfig()?.agentIdVerificationEnabled) {
+ // Add fleet global assets
+ template.composed_of = [...(template.composed_of || []), FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME];
+ }
return template;
}
@@ -456,7 +460,7 @@ function getBaseTemplate(
export const updateCurrentWriteIndices = async (
esClient: ElasticsearchClient,
- templates: TemplateRef[]
+ templates: IndexTemplateEntry[]
): Promise => {
if (!templates.length) return;
@@ -471,7 +475,7 @@ function isCurrentDataStream(item: CurrentDataStream[] | undefined): item is Cur
const queryDataStreamsFromTemplates = async (
esClient: ElasticsearchClient,
- templates: TemplateRef[]
+ templates: IndexTemplateEntry[]
): Promise => {
const dataStreamPromises = templates.map((template) => {
return getDataStreams(esClient, template);
@@ -482,7 +486,7 @@ const queryDataStreamsFromTemplates = async (
const getDataStreams = async (
esClient: ElasticsearchClient,
- template: TemplateRef
+ template: IndexTemplateEntry
): Promise => {
const { templateName, indexTemplate } = template;
const { body } = await esClient.indices.getDataStream({ name: `${templateName}-*` });
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
index 65d71ac5fdc17..1bbbb1bb9b6a2 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
@@ -10,10 +10,10 @@ import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } fro
import { MAX_TIME_COMPLETE_INSTALL, ASSETS_SAVED_OBJECT_TYPE } from '../../../../common';
import type { InstallablePackage, InstallSource, PackageAssetReference } from '../../../../common';
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
-import { ElasticsearchAssetType } from '../../../types';
import type { AssetReference, Installation, InstallType } from '../../../types';
import { installTemplates } from '../elasticsearch/template/install';
import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/';
+import { getAllTemplateRefs } from '../elasticsearch/template/install';
import { installILMPolicy } from '../elasticsearch/ilm/install';
import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install';
import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
@@ -170,10 +170,7 @@ export async function _installPackage({
installedPkg.attributes.install_version
);
}
- const installedTemplateRefs = installedTemplates.map((template) => ({
- id: template.templateName,
- type: ElasticsearchAssetType.indexTemplate,
- }));
+ const installedTemplateRefs = getAllTemplateRefs(installedTemplates);
// make sure the assets are installed (or didn't error)
if (installKibanaAssetsError) throw installKibanaAssetsError;
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts
index 28af2b563da79..6a5968441e634 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts
@@ -101,6 +101,8 @@ export async function getPackageSavedObjects(
});
}
+export const getInstallations = getPackageSavedObjects;
+
export async function getPackageInfo(options: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/index.ts b/x-pack/plugins/fleet/server/services/epm/packages/index.ts
index 608e157017e9b..1f9113590f0f7 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/index.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/index.ts
@@ -17,6 +17,7 @@ export {
getFile,
getInstallationObject,
getInstallation,
+ getInstallations,
getPackageInfo,
getPackages,
getLimitedPackages,
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
index c6fd9a8f763ab..e00526cbb4ec4 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
@@ -257,8 +257,7 @@ async function installPackageFromRegistry({
const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion);
// try installing the package, if there was an error, call error handler and rethrow
- // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status
- // @ts-ignore
+ // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed'
return _installPackage({
savedObjectsClient,
esClient,
@@ -334,8 +333,7 @@ async function installPackageByUpload({
version: packageInfo.version,
packageInfo,
});
- // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status
- // @ts-ignore
+ // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed'
return _installPackage({
savedObjectsClient,
esClient,
@@ -484,17 +482,17 @@ export const saveInstalledEsRefs = async (
return installedAssets;
};
-export const removeAssetsFromInstalledEsByType = async (
+export const removeAssetTypesFromInstalledEs = async (
savedObjectsClient: SavedObjectsClientContract,
pkgName: string,
- assetType: AssetType
+ assetTypes: AssetType[]
) => {
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
const installedAssets = installedPkg?.attributes.installed_es;
if (!installedAssets?.length) return;
- const installedAssetsToSave = installedAssets?.filter(({ id, type }) => {
- return type !== assetType;
- });
+ const installedAssetsToSave = installedAssets?.filter(
+ (asset) => !assetTypes.includes(asset.type)
+ );
return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
installed_es: installedAssetsToSave,
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
index 706f1bbbaaf35..70167d1156a66 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
@@ -89,13 +89,18 @@ function deleteKibanaAssets(
});
}
-function deleteESAssets(installedObjects: EsAssetReference[], esClient: ElasticsearchClient) {
+function deleteESAssets(
+ installedObjects: EsAssetReference[],
+ esClient: ElasticsearchClient
+): Array> {
return installedObjects.map(async ({ id, type }) => {
const assetType = type as AssetType;
if (assetType === ElasticsearchAssetType.ingestPipeline) {
return deletePipeline(esClient, id);
} else if (assetType === ElasticsearchAssetType.indexTemplate) {
- return deleteTemplate(esClient, id);
+ return deleteIndexTemplate(esClient, id);
+ } else if (assetType === ElasticsearchAssetType.componentTemplate) {
+ return deleteComponentTemplate(esClient, id);
} else if (assetType === ElasticsearchAssetType.transform) {
return deleteTransforms(esClient, [id]);
} else if (assetType === ElasticsearchAssetType.dataStreamIlmPolicy) {
@@ -111,13 +116,30 @@ async function deleteAssets(
) {
const logger = appContextService.getLogger();
- const deletePromises: Array> = [
- ...deleteESAssets(installedEs, esClient),
- ...deleteKibanaAssets(installedKibana, savedObjectsClient),
- ];
+ // must delete index templates first, or component templates which reference them cannot be deleted
+ // separate the assets into Index Templates and other assets
+ type Tuple = [EsAssetReference[], EsAssetReference[]];
+ const [indexTemplates, otherAssets] = installedEs.reduce(
+ ([indexAssetTypes, otherAssetTypes], asset) => {
+ if (asset.type === ElasticsearchAssetType.indexTemplate) {
+ indexAssetTypes.push(asset);
+ } else {
+ otherAssetTypes.push(asset);
+ }
+
+ return [indexAssetTypes, otherAssetTypes];
+ },
+ [[], []]
+ );
try {
- await Promise.all(deletePromises);
+ // must delete index templates first
+ await Promise.all(deleteESAssets(indexTemplates, esClient));
+ // then the other asset types
+ await Promise.all([
+ ...deleteESAssets(otherAssets, esClient),
+ ...deleteKibanaAssets(installedKibana, savedObjectsClient),
+ ]);
} catch (err) {
// in the rollback case, partial installs are likely, so missing assets are not an error
if (!savedObjectsClient.errors.isNotFoundError(err)) {
@@ -126,13 +148,24 @@ async function deleteAssets(
}
}
-async function deleteTemplate(esClient: ElasticsearchClient, name: string): Promise {
+async function deleteIndexTemplate(esClient: ElasticsearchClient, name: string): Promise {
// '*' shouldn't ever appear here, but it still would delete all templates
if (name && name !== '*') {
try {
await esClient.indices.deleteIndexTemplate({ name }, { ignore: [404] });
} catch {
- throw new Error(`error deleting template ${name}`);
+ throw new Error(`error deleting index template ${name}`);
+ }
+ }
+}
+
+async function deleteComponentTemplate(esClient: ElasticsearchClient, name: string): Promise {
+ // '*' shouldn't ever appear here, but it still would delete all templates
+ if (name && name !== '*') {
+ try {
+ await esClient.cluster.deleteComponentTemplate({ name }, { ignore: [404] });
+ } catch (error) {
+ throw new Error(`error deleting component template ${name}`);
}
}
}
diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts
index 0c7b086f78fdf..8c6bc7eca0401 100644
--- a/x-pack/plugins/fleet/server/services/output.ts
+++ b/x-pack/plugins/fleet/server/services/output.ts
@@ -9,10 +9,9 @@ import type { SavedObjectsClientContract } from 'src/core/server';
import type { NewOutput, Output, OutputSOAttributes } from '../types';
import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants';
-import { decodeCloudId } from '../../common';
+import { decodeCloudId, normalizeHostsForAgents } from '../../common';
import { appContextService } from './app_context';
-import { normalizeHostsForAgents } from './hosts_utils';
const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE;
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts
index a8be94ca61c0a..e016fafe5459d 100644
--- a/x-pack/plugins/fleet/server/services/preconfiguration.ts
+++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts
@@ -108,7 +108,7 @@ export async function ensurePreconfiguredPackagesAndPolicies(
policies.map(async (preconfiguredAgentPolicy) => {
if (preconfiguredAgentPolicy.id) {
// Check to see if a preconfigured policy with the same preconfiguration id was already deleted by the user
- const preconfigurationId = String(preconfiguredAgentPolicy.id);
+ const preconfigurationId = preconfiguredAgentPolicy.id.toString();
const searchParams = {
searchFields: ['id'],
search: escapeSearchQueryPhrase(preconfigurationId),
diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts
index 226fbb29467c2..26d581f32d9a2 100644
--- a/x-pack/plugins/fleet/server/services/settings.ts
+++ b/x-pack/plugins/fleet/server/services/settings.ts
@@ -8,11 +8,14 @@
import Boom from '@hapi/boom';
import type { SavedObjectsClientContract } from 'kibana/server';
-import { decodeCloudId, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '../../common';
+import {
+ decodeCloudId,
+ GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
+ normalizeHostsForAgents,
+} from '../../common';
import type { SettingsSOAttributes, Settings, BaseSettings } from '../../common';
import { appContextService } from './app_context';
-import { normalizeHostsForAgents } from './hosts_utils';
export async function getSettings(soClient: SavedObjectsClientContract): Promise {
const res = await soClient.find({
diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts
index 45805bb066c3b..cfef04846d92e 100644
--- a/x-pack/plugins/fleet/server/services/setup.ts
+++ b/x-pack/plugins/fleet/server/services/setup.ts
@@ -24,7 +24,10 @@ import { awaitIfPending } from './setup_utils';
import { ensureAgentActionPolicyChangeExists } from './agents';
import { awaitIfFleetServerSetupPending } from './fleet_server';
import { ensureFleetFinalPipelineIsInstalled } from './epm/elasticsearch/ingest_pipeline/install';
+import { ensureDefaultComponentTemplate } from './epm/elasticsearch/template/install';
+import { getInstallations, installPackage } from './epm/packages';
import { isPackageInstalled } from './epm/packages/install';
+import { pkgToPkgKey } from './epm/registry';
export interface SetupStatus {
isInitialized: boolean;
@@ -47,9 +50,10 @@ async function createSetupSideEffects(
settingsService.settingsSetup(soClient),
]);
- await ensureFleetFinalPipelineIsInstalled(esClient);
-
await awaitIfFleetServerSetupPending();
+ if (appContextService.getConfig()?.agentIdVerificationEnabled) {
+ await ensureFleetGlobalEsAssets(soClient, esClient);
+ }
const { agentPolicies: policiesOrUndefined, packages: packagesOrUndefined } =
appContextService.getConfig() ?? {};
@@ -95,6 +99,49 @@ async function createSetupSideEffects(
};
}
+/**
+ * Ensure ES assets shared by all Fleet index template are installed
+ */
+export async function ensureFleetGlobalEsAssets(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient
+) {
+ const logger = appContextService.getLogger();
+ // Ensure Global Fleet ES assets are installed
+ const globalAssetsRes = await Promise.all([
+ ensureDefaultComponentTemplate(esClient),
+ ensureFleetFinalPipelineIsInstalled(esClient),
+ ]);
+
+ if (globalAssetsRes.some((asset) => asset.isCreated)) {
+ // Update existing index template
+ const packages = await getInstallations(soClient);
+
+ await Promise.all(
+ packages.saved_objects.map(async ({ attributes: installation }) => {
+ if (installation.install_source !== 'registry') {
+ logger.error(
+ `Package needs to be manually reinstalled ${installation.name} after installing Fleet global assets`
+ );
+ return;
+ }
+ await installPackage({
+ installSource: installation.install_source,
+ savedObjectsClient: soClient,
+ pkgkey: pkgToPkgKey({ name: installation.name, version: installation.version }),
+ esClient,
+ // Force install the pacakge will update the index template and the datastream write indices
+ force: true,
+ }).catch((err) => {
+ logger.error(
+ `Package needs to be manually reinstalled ${installation.name} after installing Fleet global assets: ${err.message}`
+ );
+ });
+ })
+ );
+ }
+}
+
export async function ensureDefaultEnrollmentAPIKeysExists(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx
index 8927676976457..0c08a09e76f4e 100644
--- a/x-pack/plugins/fleet/server/types/index.tsx
+++ b/x-pack/plugins/fleet/server/types/index.tsx
@@ -63,7 +63,7 @@ export {
IndexTemplate,
RegistrySearchResults,
RegistrySearchResult,
- TemplateRef,
+ IndexTemplateEntry,
IndexTemplateMappings,
Settings,
SettingsSOAttributes,
diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts
index db551b25e9ebb..48aea1b5cbcc4 100644
--- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts
@@ -16,6 +16,7 @@ export const AgentPolicyBaseSchema = {
namespace: NamespaceSchema,
description: schema.maybe(schema.string()),
is_managed: schema.maybe(schema.boolean()),
+ unenroll_timeout: schema.maybe(schema.number({ min: 1 })),
monitoring_enabled: schema.maybe(
schema.arrayOf(
schema.oneOf([schema.literal(dataTypes.Logs), schema.literal(dataTypes.Metrics)])
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
index 6254a6512efb5..9595009347259 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
@@ -314,89 +314,99 @@ exports[`extend index management ilm summary extension should return extension w
- illegal_argument_exception
- :
- setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined
-
-
-
+ illegal_argument_exception
+ :
+ setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined
+
-
-
- }
- closePopover={[Function]}
- display="inlineBlock"
- hasArrow={true}
- id="stackPopover"
- isOpen={false}
- ownFocus={true}
- panelPaddingSize="m"
- >
-
-
+
+
+
+ }
+ closePopover={[Function]}
+ display="inlineBlock"
+ hasArrow={true}
+ id="stackPopover"
+ isOpen={false}
+ ownFocus={true}
+ panelPaddingSize="m"
>
-
-
-
-
-
-
- Stack trace
-
-
-
-
-
-
-
+
+
+ Stack trace
+
+
+
+
+
+
+
+
+
-
+
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
index 556ac35d0565e..4d2b47c8a6039 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
@@ -58,16 +58,16 @@ exports[`policy table should show empty state when there are not any policies 1`
data-euiicon-type="managementApp"
/>
+
+ Create your first index lifecycle policy
+
-
- Create your first index lifecycle policy
-
@@ -82,9 +82,6 @@ exports[`policy table should show empty state when there are not any policies 1`
-
{
return new IndexLifecycleManagementPlugin(initializerContext);
};
-export { ILM_URL_GENERATOR_ID, IlmUrlGeneratorState } from './url_generator';
+export { ILM_LOCATOR_ID, IlmLocatorParams } from './locator';
diff --git a/x-pack/plugins/index_lifecycle_management/public/locator.ts b/x-pack/plugins/index_lifecycle_management/public/locator.ts
new file mode 100644
index 0000000000000..025946a095a6f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/locator.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SerializableState } from 'src/plugins/kibana_utils/common';
+import { ManagementAppLocator } from 'src/plugins/management/common';
+import { LocatorDefinition } from '../../../../src/plugins/share/public/';
+import {
+ getPoliciesListPath,
+ getPolicyCreatePath,
+ getPolicyEditPath,
+} from './application/services/navigation';
+import { PLUGIN } from '../common/constants';
+
+export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID';
+
+export interface IlmLocatorParams extends SerializableState {
+ page: 'policies_list' | 'policy_edit' | 'policy_create';
+ policyName?: string;
+}
+
+export interface IlmLocatorDefinitionDependencies {
+ managementAppLocator: ManagementAppLocator;
+}
+
+export class IlmLocatorDefinition implements LocatorDefinition {
+ constructor(protected readonly deps: IlmLocatorDefinitionDependencies) {}
+
+ public readonly id = ILM_LOCATOR_ID;
+
+ public readonly getLocation = async (params: IlmLocatorParams) => {
+ const location = await this.deps.managementAppLocator.getLocation({
+ sectionId: 'data',
+ appId: PLUGIN.ID,
+ });
+
+ switch (params.page) {
+ case 'policy_create': {
+ return {
+ ...location,
+ path: location.path + getPolicyCreatePath(),
+ };
+ }
+ case 'policy_edit': {
+ return {
+ ...location,
+ path: location.path + getPolicyEditPath(params.policyName!),
+ };
+ }
+ case 'policies_list': {
+ return {
+ ...location,
+ path: location.path + getPoliciesListPath(),
+ };
+ }
+ }
+ };
+}
diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx
index 069d1e0d10e0b..163fe2b3d9b5c 100644
--- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx
@@ -17,7 +17,7 @@ import { init as initNotification } from './application/services/notification';
import { BreadcrumbService } from './application/services/breadcrumbs';
import { addAllExtensions } from './extend_index_management';
import { ClientConfigType, SetupDependencies, StartDependencies } from './types';
-import { registerUrlGenerator } from './url_generator';
+import { IlmLocatorDefinition } from './locator';
export class IndexLifecycleManagementPlugin
implements Plugin {
@@ -38,7 +38,7 @@ export class IndexLifecycleManagementPlugin
getStartServices,
} = coreSetup;
- const { usageCollection, management, indexManagement, home, cloud, share } = plugins;
+ const { usageCollection, management, indexManagement, home, cloud } = plugins;
// Initialize services even if the app isn't mounted, because they're used by index management extensions.
initHttp(http);
@@ -110,7 +110,11 @@ export class IndexLifecycleManagementPlugin
addAllExtensions(indexManagement.extensionsService);
}
- registerUrlGenerator(coreSetup, management, share);
+ plugins.share.url.locators.create(
+ new IlmLocatorDefinition({
+ managementAppLocator: plugins.management.locator,
+ })
+ );
}
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/url_generator.ts b/x-pack/plugins/index_lifecycle_management/public/url_generator.ts
deleted file mode 100644
index f7794c535198f..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/url_generator.ts
+++ /dev/null
@@ -1,61 +0,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 'kibana/public';
-import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public/';
-import {
- getPoliciesListPath,
- getPolicyCreatePath,
- getPolicyEditPath,
-} from './application/services/navigation';
-import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public';
-import { SetupDependencies } from './types';
-import { PLUGIN } from '../common/constants';
-
-export const ILM_URL_GENERATOR_ID = 'ILM_URL_GENERATOR_ID';
-
-export interface IlmUrlGeneratorState {
- page: 'policies_list' | 'policy_edit' | 'policy_create';
- policyName?: string;
- absolute?: boolean;
-}
-export const createIlmUrlGenerator = (
- getAppBasePath: (absolute?: boolean) => Promise
-): UrlGeneratorsDefinition => {
- return {
- id: ILM_URL_GENERATOR_ID,
- createUrl: async (state: IlmUrlGeneratorState): Promise => {
- switch (state.page) {
- case 'policy_create': {
- return `${await getAppBasePath(!!state.absolute)}${getPolicyCreatePath()}`;
- }
- case 'policy_edit': {
- return `${await getAppBasePath(!!state.absolute)}${getPolicyEditPath(state.policyName!)}`;
- }
- case 'policies_list': {
- return `${await getAppBasePath(!!state.absolute)}${getPoliciesListPath()}`;
- }
- }
- },
- };
-};
-
-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.data.getApp(PLUGIN.ID)!.basePath,
- absolute,
- });
- };
-
- share.urlGenerators.registerUrlGenerator(createIlmUrlGenerator(getAppBasePath));
-};
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
index 93cd772ce6658..8e114b0596948 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
@@ -22,6 +22,21 @@ import {
const nonBreakingSpace = ' ';
+const urlServiceMock = {
+ locators: {
+ get: () => ({
+ getLocation: async () => ({
+ app: '',
+ path: '',
+ state: {},
+ }),
+ getUrl: async ({ policyName }: { policyName: string }) => `/test/${policyName}`,
+ navigate: async () => {},
+ useUrl: () => '',
+ }),
+ },
+};
+
describe('Data Streams tab', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
let testBed: DataStreamsTabTestBed;
@@ -38,7 +53,9 @@ describe('Data Streams tab', () => {
});
test('displays an empty prompt', async () => {
- testBed = await setup();
+ testBed = await setup({
+ url: urlServiceMock,
+ });
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -54,6 +71,7 @@ describe('Data Streams tab', () => {
test('when Ingest Manager is disabled, goes to index templates tab when "Get started" link is clicked', async () => {
testBed = await setup({
plugins: {},
+ url: urlServiceMock,
});
await act(async () => {
@@ -73,6 +91,7 @@ describe('Data Streams tab', () => {
test('when Fleet is enabled, links to Fleet', async () => {
testBed = await setup({
plugins: { isFleetEnabled: true },
+ url: urlServiceMock,
});
await act(async () => {
@@ -95,6 +114,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
plugins: {},
+ url: urlServiceMock,
});
await act(async () => {
@@ -345,6 +365,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
+ url: urlServiceMock,
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -370,13 +391,8 @@ describe('Data Streams tab', () => {
});
});
- describe('url generators', () => {
- const mockIlmUrlGenerator = {
- getUrlGenerator: () => ({
- createUrl: ({ policyName }: { policyName: string }) => `/test/${policyName}`,
- }),
- };
- test('with an ILM url generator and an ILM policy', async () => {
+ describe('url locators', () => {
+ test('with an ILM url locator and an ILM policy', async () => {
const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers;
const dataStreamForDetailPanel = createDataStreamPayload({
@@ -388,7 +404,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
- urlGenerators: mockIlmUrlGenerator,
+ url: urlServiceMock,
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -400,7 +416,7 @@ describe('Data Streams tab', () => {
expect(findDetailPanelIlmPolicyLink().prop('href')).toBe('/test/my_ilm_policy');
});
- test('with an ILM url generator and no ILM policy', async () => {
+ test('with an ILM url locator and no ILM policy', async () => {
const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers;
const dataStreamForDetailPanel = createDataStreamPayload({ name: 'dataStream1' });
@@ -409,7 +425,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
- urlGenerators: mockIlmUrlGenerator,
+ url: urlServiceMock,
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -422,7 +438,7 @@ describe('Data Streams tab', () => {
expect(findDetailPanelIlmPolicyName().contains('None')).toBeTruthy();
});
- test('without an ILM url generator and with an ILM policy', async () => {
+ test('without an ILM url locator and with an ILM policy', async () => {
const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers;
const dataStreamForDetailPanel = createDataStreamPayload({
@@ -434,7 +450,11 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
- urlGenerators: { getUrlGenerator: () => {} },
+ url: {
+ locators: {
+ get: () => undefined,
+ },
+ },
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -463,6 +483,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
+ url: urlServiceMock,
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -506,6 +527,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
+ url: urlServiceMock,
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -542,7 +564,7 @@ describe('Data Streams tab', () => {
beforeEach(async () => {
setLoadDataStreamsResponse([dataStreamWithDelete, dataStreamNoDelete]);
- testBed = await setup({ history: createMemoryHistory() });
+ testBed = await setup({ history: createMemoryHistory(), url: urlServiceMock });
await act(async () => {
testBed.actions.goToDataStreamsList();
});
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 4ac94319d4711..463d0b30cad08 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/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx
index 3b06d76cf7c26..f8ebfdf7c46b7 100644
--- a/x-pack/plugins/index_management/public/application/app_context.tsx
+++ b/x-pack/plugins/index_management/public/application/app_context.tsx
@@ -35,7 +35,7 @@ export interface AppDependencies {
history: ScopedHistory;
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
uiSettings: CoreSetup['uiSettings'];
- urlGenerators: SharePluginStart['urlGenerators'];
+ url: SharePluginStart['url'];
docLinks: CoreStart['docLinks'];
}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
index 8c8f7e5789925..dee15f2ae3a45 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
@@ -165,8 +165,10 @@ describe(' ', () => {
const { exists, find } = testBed;
expect(exists('componentTemplatesLoadError')).toBe(true);
+ // The text here looks weird because the child elements' text values (title and description)
+ // are concatenated when we retrive the error element's text value.
expect(find('componentTemplatesLoadError').text()).toContain(
- 'Unable to load component templates. Try again.'
+ 'Error loading component templatesInternal server error'
);
});
});
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
index 2bb240e6b6ae1..77668f7d55072 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
@@ -13,8 +13,13 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { ScopedHistory } from 'kibana/public';
import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
-import { attemptToURIDecode } from '../../../../shared_imports';
-import { SectionLoading, ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports';
+import {
+ APP_WRAPPER_CLASS,
+ PageLoading,
+ PageError,
+ attemptToURIDecode,
+} from '../../../../shared_imports';
+import { ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports';
import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants';
import { useComponentTemplatesContext } from '../component_templates_context';
import {
@@ -24,7 +29,6 @@ import {
} from '../component_template_details';
import { EmptyPrompt } from './empty_prompt';
import { ComponentTable } from './table';
-import { LoadError } from './error';
import { ComponentTemplatesDeleteModal } from './delete_modal';
interface Props {
@@ -138,18 +142,20 @@ export const ComponentTemplateList: React.FunctionComponent = ({
}
}, [componentTemplateName, removeContentFromGlobalFlyout]);
- let content: React.ReactNode;
-
if (isLoading) {
- content = (
-
+ return (
+
-
+
);
- } else if (data?.length) {
+ }
+
+ let content: React.ReactNode;
+
+ if (data?.length) {
content = (
<>
@@ -183,11 +189,22 @@ export const ComponentTemplateList: React.FunctionComponent = ({
} else if (data && data.length === 0) {
content = ;
} else if (error) {
- content = ;
+ content = (
+
+ }
+ error={error}
+ data-test-subj="componentTemplatesLoadError"
+ />
+ );
}
return (
-
+
{content}
{/* delete modal */}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx
deleted file mode 100644
index 9fd0031fe8778..0000000000000
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { FunctionComponent } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiLink, EuiCallOut } from '@elastic/eui';
-
-export interface Props {
- onReloadClick: () => void;
-}
-
-export const LoadError: FunctionComponent
= ({ onReloadClick }) => {
- return (
-
-
-
- ),
- }}
- />
- }
- />
- );
-};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx
index a0f6dc4b59fe7..eecb56768df9a 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx
@@ -9,10 +9,10 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { FunctionComponent } from 'react';
import {
- SectionError,
+ PageLoading,
+ PageError,
useAuthorizationContext,
WithPrivileges,
- SectionLoading,
NotAuthorizedSection,
} from '../shared_imports';
import { APP_CLUSTER_REQUIRED_PRIVILEGES } from '../constants';
@@ -26,7 +26,7 @@ export const ComponentTemplatesWithPrivileges: FunctionComponent = ({
if (apiError) {
return (
- {
if (isLoading) {
return (
-
+
-
+
);
}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx
index b87b043c924a6..d19c500c3622a 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx
@@ -10,7 +10,7 @@ import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { SectionLoading, attemptToURIDecode } from '../../shared_imports';
+import { PageLoading, attemptToURIDecode } from '../../shared_imports';
import { useComponentTemplatesContext } from '../../component_templates_context';
import { ComponentTemplateCreate } from '../component_template_create';
@@ -30,7 +30,8 @@ export const ComponentTemplateClone: FunctionComponent {
if (error && !isLoading) {
- toasts.addError(error, {
+ // Toasts expects a generic Error object, which is typed as having a required name property.
+ toasts.addError({ ...error, name: '' } as Error, {
title: i18n.translate('xpack.idxMgmt.componentTemplateClone.loadComponentTemplateTitle', {
defaultMessage: `Error loading component template '{sourceComponentTemplateName}'.`,
values: { sourceComponentTemplateName },
@@ -42,12 +43,12 @@ export const ComponentTemplateClone: FunctionComponent
+
-
+
);
} else {
// We still show the create form (unpopulated) even if we were not able to load the
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx
index 5163c75bdbadd..8fe2c193daa0c 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx
@@ -8,7 +8,7 @@
import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody, EuiSpacer, EuiPageHeader } from '@elastic/eui';
import { ComponentTemplateDeserialized } from '../../shared_imports';
import { useComponentTemplatesContext } from '../../component_templates_context';
@@ -59,27 +59,28 @@ export const ComponentTemplateCreate: React.FunctionComponent
-
-
-
+
+
-
-
-
-
-
-
-
-
+
+ }
+ bottomBorder
+ />
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx
index 809fac980069f..6ac831b5dacce 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx
@@ -8,13 +8,15 @@
import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui';
+import { EuiPageContentBody, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { useComponentTemplatesContext } from '../../component_templates_context';
import {
ComponentTemplateDeserialized,
- SectionLoading,
+ PageLoading,
+ PageError,
attemptToURIDecode,
+ Error,
} from '../../shared_imports';
import { ComponentTemplateForm } from '../component_template_form';
@@ -65,64 +67,57 @@ export const ComponentTemplateEdit: React.FunctionComponent
+ return (
+
-
- );
- } else if (error) {
- content = (
- <>
-
- }
- color="danger"
- iconType="alert"
- data-test-subj="loadComponentTemplateError"
- >
- {error.message}
-
-
- >
+
);
- } else if (componentTemplate) {
- content = (
-
+ }
+ error={error as Error}
+ data-test-subj="loadComponentTemplateError"
/>
);
}
return (
-
-
-
-
+
+
-
-
-
- {content}
-
-
+
+ }
+ bottomBorder
+ />
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts
index 75c68e71996b8..6bf6d204fd9a5 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts
@@ -10,7 +10,6 @@ import {
ComponentTemplateListItem,
ComponentTemplateDeserialized,
ComponentTemplateSerialized,
- Error,
} from '../shared_imports';
import {
UIM_COMPONENT_TEMPLATE_DELETE_MANY,
@@ -26,7 +25,7 @@ export const getApi = (
trackMetric: (type: UiCounterMetricType, eventName: string) => void
) => {
function useLoadComponentTemplates() {
- return useRequest({
+ return useRequest({
path: `${apiBasePath}/component_templates`,
method: 'get',
});
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts
index 64b2e6b47e5d9..a7056e27b5cad 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts
@@ -14,6 +14,7 @@ import {
SendRequestResponse,
sendRequest as _sendRequest,
useRequest as _useRequest,
+ Error,
} from '../shared_imports';
export type UseRequestHook = (
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts
index afc7aed874387..15528f5b4e8e5 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts
@@ -12,10 +12,12 @@ export {
SendRequestResponse,
sendRequest,
useRequest,
- SectionLoading,
WithPrivileges,
AuthorizationProvider,
SectionError,
+ SectionLoading,
+ PageLoading,
+ PageError,
Error,
useAuthorizationContext,
NotAuthorizedSection,
diff --git a/x-pack/plugins/index_management/public/application/components/index.ts b/x-pack/plugins/index_management/public/application/components/index.ts
index f5c58e5b45ebd..eeba6e16b543c 100644
--- a/x-pack/plugins/index_management/public/application/components/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/index.ts
@@ -6,9 +6,7 @@
*/
export { SectionError, Error } from './section_error';
-export { SectionLoading } from './section_loading';
export { NoMatch } from './no_match';
-export { PageErrorForbidden } from './page_error';
export { TemplateDeleteModal } from './template_delete_modal';
export { TemplateForm } from './template_form';
export { DataHealth } from './data_health';
diff --git a/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx b/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx
deleted file mode 100644
index e22b180881ed5..0000000000000
--- a/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import { EuiEmptyPrompt, EuiPageContent } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-export function PageErrorForbidden() {
- return (
-
-
-
-
- }
- />
-
- );
-}
diff --git a/x-pack/plugins/index_management/public/application/components/section_loading.tsx b/x-pack/plugins/index_management/public/application/components/section_loading.tsx
deleted file mode 100644
index 3c31744dee398..0000000000000
--- a/x-pack/plugins/index_management/public/application/components/section_loading.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui';
-
-interface Props {
- children: React.ReactNode;
-}
-
-export const SectionLoading: React.FunctionComponent = ({ children }) => {
- return (
- }
- body={{children} }
- data-test-subj="sectionLoading"
- />
- );
-};
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
index 54160141827d0..4ccd77d275a94 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
@@ -8,7 +8,7 @@
import React, { useState, useCallback, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiSpacer, EuiButton } from '@elastic/eui';
+import { EuiSpacer, EuiButton, EuiPageHeader } from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import { TemplateDeserialized } from '../../../../common';
@@ -292,7 +292,7 @@ export const TemplateForm = ({
return (
<>
{/* Form header */}
- {title}
+ {title}} bottomBorder />
diff --git a/x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts b/x-pack/plugins/index_management/public/application/constants/ilm_locator.ts
similarity index 83%
rename from x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts
rename to x-pack/plugins/index_management/public/application/constants/ilm_locator.ts
index ea6cf1756b73c..3da13727af8de 100644
--- a/x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts
+++ b/x-pack/plugins/index_management/public/application/constants/ilm_locator.ts
@@ -5,5 +5,5 @@
* 2.0.
*/
-export const ILM_URL_GENERATOR_ID = 'ILM_URL_GENERATOR_ID';
+export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID';
export const ILM_PAGES_POLICY_EDIT = 'policy_edit';
diff --git a/x-pack/plugins/index_management/public/application/constants/index.ts b/x-pack/plugins/index_management/public/application/constants/index.ts
index 3bf30517c1145..7a1caf5e50771 100644
--- a/x-pack/plugins/index_management/public/application/constants/index.ts
+++ b/x-pack/plugins/index_management/public/application/constants/index.ts
@@ -17,4 +17,4 @@ export {
export const REACT_ROOT_ID = 'indexManagementReactRoot';
-export * from './ilm_url_generator';
+export * from './ilm_locator';
diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts
index 074334ed87725..083a8831291dd 100644
--- a/x-pack/plugins/index_management/public/application/mount_management_section.ts
+++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts
@@ -62,7 +62,7 @@ export async function mountManagementSection(
uiSettings,
} = core;
- const { urlGenerators } = startDependencies.share;
+ const { url } = startDependencies.share;
docTitle.change(PLUGIN.getI18nName(i18n));
breadcrumbService.setup(setBreadcrumbs);
@@ -86,7 +86,7 @@ export async function mountManagementSection(
history,
setBreadcrumbs,
uiSettings,
- urlGenerators,
+ url,
docLinks,
};
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 773ccd91a5fb1..3d5f56c08f8e1 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
@@ -24,16 +24,16 @@ 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';
-import { useUrlGenerator } from '../../../../services/use_url_generator';
import { getIndexListUri, getTemplateDetailsLink } from '../../../../services/routing';
-import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../constants';
+import { ILM_PAGES_POLICY_EDIT } from '../../../../constants';
import { useAppContext } from '../../../../app_context';
import { DataStreamsBadges } from '../data_stream_badges';
+import { useIlmLocator } from '../../../../services/use_ilm_locator';
interface DetailsListProps {
details: Array<{
@@ -89,13 +89,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({
const [isDeleting, setIsDeleting] = useState(false);
- const ilmPolicyLink = useUrlGenerator({
- urlGeneratorId: ILM_URL_GENERATOR_ID,
- urlGeneratorState: {
- page: ILM_PAGES_POLICY_EDIT,
- policyName: dataStream?.ilmPolicyName,
- },
- });
+ const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, dataStream?.ilmPolicyName);
const { history } = useAppContext();
let content;
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
index 131dc2662bc1c..7bd7c163837d8 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
@@ -16,18 +16,22 @@ import {
EuiText,
EuiIconTip,
EuiSpacer,
+ EuiPageContent,
EuiEmptyPrompt,
EuiLink,
} from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import {
+ PageLoading,
+ PageError,
+ Error,
reactRouterNavigate,
extractQueryParams,
attemptToURIDecode,
+ APP_WRAPPER_CLASS,
} from '../../../../shared_imports';
import { useAppContext } from '../../../app_context';
-import { SectionError, SectionLoading, Error } from '../../../components';
import { useLoadDataStreams } from '../../../services/api';
import { documentationService } from '../../../services/documentation';
import { Section } from '../home';
@@ -166,16 +170,16 @@ export const DataStreamList: React.FunctionComponent
+
-
+
);
} else if (error) {
content = (
-
);
- } else if (Array.isArray(dataStreams) && dataStreams.length > 0) {
- activateHiddenFilter(isSelectedDataStreamHidden(dataStreams, decodedDataStreamName));
+ } else {
+ activateHiddenFilter(isSelectedDataStreamHidden(dataStreams!, decodedDataStreamName));
content = (
- <>
+
{renderHeader()}
@@ -270,12 +274,12 @@ export const DataStreamList: React.FunctionComponent
- >
+
);
}
return (
-
+
{content}
{/*
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx
index ac46b5dbd256b..fc68ca33e9536 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx
@@ -8,12 +8,13 @@
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
+import { APP_WRAPPER_CLASS } from '../../../../shared_imports';
import { DetailPanel } from './detail_panel';
import { IndexTable } from './index_table';
export const IndexList: React.FunctionComponent
= ({ history }) => {
return (
-
+
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
index f488290692e7e..0a407927e3466 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
@@ -19,7 +19,7 @@ import {
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
- EuiLoadingSpinner,
+ EuiPageContent,
EuiScreenReaderOnly,
EuiSpacer,
EuiSearchBar,
@@ -37,13 +37,18 @@ import {
} from '@elastic/eui';
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants';
-import { reactRouterNavigate, attemptToURIDecode } from '../../../../../shared_imports';
+import {
+ PageLoading,
+ PageError,
+ reactRouterNavigate,
+ attemptToURIDecode,
+} from '../../../../../shared_imports';
import { REFRESH_RATE_INDEX_LIST } from '../../../../constants';
import { getDataStreamDetailsLink } from '../../../../services/routing';
import { documentationService } from '../../../../services/documentation';
import { AppContextConsumer } from '../../../../app_context';
import { renderBadges } from '../../../../lib/render_badges';
-import { NoMatch, PageErrorForbidden, DataHealth } from '../../../../components';
+import { NoMatch, DataHealth } from '../../../../components';
import { IndexActionsContextMenu } from '../index_actions_context_menu';
const HEADERS = {
@@ -332,42 +337,6 @@ export class IndexTable extends Component {
});
}
- renderError() {
- const { indicesError } = this.props;
-
- const data = indicesError.body ? indicesError.body : indicesError;
-
- const { error: errorString, cause, message } = data;
-
- return (
-
-
- }
- color="danger"
- iconType="alert"
- >
- {message || errorString}
- {cause && (
-
-
-
- {cause.map((message, i) => (
- {message}
- ))}
-
-
- )}
-
-
-
- );
- }
-
renderBanners(extensionsService) {
const { allIndices = [], filterChanged } = this.props;
return extensionsService.banners.map((bannerExtension, i) => {
@@ -470,37 +439,71 @@ export class IndexTable extends Component {
} = this.props;
const { includeHiddenIndices } = this.readURLParams();
+ const hasContent = !indicesLoading && !indicesError;
- let emptyState;
+ if (!hasContent) {
+ const renderNoContent = () => {
+ if (indicesLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (indicesError) {
+ if (indicesError.status === 403) {
+ return (
+
+ }
+ />
+ );
+ }
- if (indicesLoading) {
- emptyState = (
-
-
-
-
-
- );
- }
+ return (
+
+ }
+ error={indicesError.body}
+ />
+ );
+ }
+ };
- if (!indicesLoading && !indicesError) {
- emptyState =
;
+ return (
+
+ {renderNoContent()}
+
+ );
}
const { selectedIndicesMap } = this.state;
const atLeastOneItemSelected = Object.keys(selectedIndicesMap).length > 0;
- if (indicesError && indicesError.status === 403) {
- return
;
- }
-
return (
{({ services }) => {
const { extensionsService } = services;
return (
-
+
@@ -557,8 +560,6 @@ export class IndexTable extends Component {
{this.renderBanners(extensionsService)}
- {indicesError && this.renderError()}
-
{atLeastOneItemSelected ? (
@@ -665,13 +666,13 @@ export class IndexTable extends Component {
) : (
- emptyState
+
)}
{indices.length > 0 ? this.renderPager() : null}
-
+
);
}}
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
index 2dd2c6e30cfcc..c17ccd9ced932 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
@@ -21,8 +21,8 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { TemplateDeserialized } from '../../../../../../../common';
-import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../../constants';
-import { useUrlGenerator } from '../../../../../services/use_url_generator';
+import { ILM_PAGES_POLICY_EDIT } from '../../../../../constants';
+import { useIlmLocator } from '../../../../../services/use_ilm_locator';
interface Props {
templateDetails: TemplateDeserialized;
@@ -54,13 +54,7 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails })
const numIndexPatterns = indexPatterns.length;
- const ilmPolicyLink = useUrlGenerator({
- urlGeneratorId: ILM_URL_GENERATOR_ID,
- urlGeneratorState: {
- page: ILM_PAGES_POLICY_EDIT,
- policyName: ilmPolicy?.name,
- },
- });
+ const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, ilmPolicy?.name);
return (
<>
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx
index e61362efb8c99..1a82cb3bfbdd1 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx
@@ -33,8 +33,8 @@ import {
UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB,
} from '../../../../../../common/constants';
-import { UseRequestResponse } from '../../../../../shared_imports';
-import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components';
+import { SectionLoading, UseRequestResponse } from '../../../../../shared_imports';
+import { TemplateDeleteModal, SectionError, Error } from '../../../../components';
import { useLoadIndexTemplate } from '../../../../services/api';
import { useServices } from '../../../../app_context';
import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared';
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
index b8b5a8e3c7d1a..57f18134be5d6 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Fragment, useState, useEffect, useMemo } from 'react';
+import React, { useState, useEffect, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -24,13 +24,14 @@ import {
import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants';
import { TemplateListItem } from '../../../../../common';
-import { attemptToURIDecode } from '../../../../shared_imports';
import {
- SectionError,
- SectionLoading,
- Error,
- LegacyIndexTemplatesDeprecation,
-} from '../../../components';
+ APP_WRAPPER_CLASS,
+ PageLoading,
+ PageError,
+ attemptToURIDecode,
+ reactRouterNavigate,
+} from '../../../../shared_imports';
+import { LegacyIndexTemplatesDeprecation } from '../../../components';
import { useLoadIndexTemplates } from '../../../services/api';
import { documentationService } from '../../../services/documentation';
import { useServices } from '../../../app_context';
@@ -130,7 +131,8 @@ export const TemplateList: React.FunctionComponent (
-
+ // flex-grow: 0 is needed here because the parent element is a flex column and the header would otherwise expand.
+
);
- const renderContent = () => {
- if (isLoading) {
- return (
-
+ // Track this component mounted.
+ useEffect(() => {
+ uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD);
+ }, [uiMetricService]);
+
+ let content;
+
+ if (isLoading) {
+ content = (
+
+
+
+ );
+ } else if (error) {
+ content = (
+
-
- );
- } else if (error) {
- return (
-
+ );
+ } else if (!hasTemplates) {
+ content = (
+
- }
- error={error as Error}
- />
- );
- } else if (!hasTemplates) {
- return (
-
+
+ }
+ body={
+ <>
+
-
- }
- data-test-subj="emptyPrompt"
- />
- );
- } else {
- return (
-
- {/* Header */}
- {renderHeader()}
+
+ >
+ }
+ actions={
+
+
+
+ }
+ data-test-subj="emptyPrompt"
+ />
+ );
+ } else {
+ content = (
+ <>
+ {/* Header */}
+ {renderHeader()}
- {/* Composable index templates table */}
- {renderTemplatesTable()}
+ {/* Composable index templates table */}
+ {renderTemplatesTable()}
- {/* Legacy index templates table. We discourage their adoption if the user isn't already using them. */}
- {filteredTemplates.legacyTemplates.length > 0 && renderLegacyTemplatesTable()}
-
- );
- }
- };
+ {/* Legacy index templates table. We discourage their adoption if the user isn't already using them. */}
+ {filteredTemplates.legacyTemplates.length > 0 && renderLegacyTemplatesTable()}
- // Track component loaded
- useEffect(() => {
- uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD);
- }, [uiMetricService]);
+ {isTemplateDetailsVisible && (
+
+ )}
+ >
+ );
+ }
return (
-
- {renderContent()}
-
- {isTemplateDetailsVisible && (
-
- )}
+
+ {content}
);
};
diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
index 36bff298e345b..32c84bc3b15f1 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
@@ -8,11 +8,12 @@
import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody } from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
+import { PageLoading, PageError, Error } from '../../../shared_imports';
import { TemplateDeserialized } from '../../../../common';
-import { TemplateForm, SectionLoading, SectionError, Error } from '../../components';
+import { TemplateForm } from '../../components';
import { breadcrumbService } from '../../services/breadcrumbs';
import { getTemplateDetailsLink } from '../../services/routing';
import { saveTemplate, useLoadIndexTemplate } from '../../services/api';
@@ -62,24 +63,22 @@ export const TemplateClone: React.FunctionComponent
{
breadcrumbService.setBreadcrumbs('templateClone');
}, []);
if (isLoading) {
- content = (
-
+ return (
+
-
+
);
} else if (templateToCloneError) {
- content = (
-
);
- } else if (templateToClone) {
- const templateData = {
- ...templateToClone,
- name: `${decodedTemplateName}-copy`,
- } as TemplateDeserialized;
+ }
+
+ const templateData = {
+ ...templateToClone,
+ name: `${decodedTemplateName}-copy`,
+ } as TemplateDeserialized;
- content = (
+ return (
+
-
-
-
-
+
}
defaultValue={templateData}
onSave={onSave}
@@ -117,12 +114,6 @@ export const TemplateClone: React.FunctionComponent
- );
- }
-
- return (
-
- {content}
-
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
index 310807aeef38f..6eba112b11939 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
@@ -8,7 +8,7 @@
import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody } from '@elastic/eui';
import { useLocation } from 'react-router-dom';
import { parse } from 'query-string';
import { ScopedHistory } from 'kibana/public';
@@ -52,34 +52,28 @@ export const TemplateCreate: React.FunctionComponent = ({ h
}, []);
return (
-
-
-
-
- {isLegacy ? (
-
- ) : (
-
- )}
-
-
- }
- onSave={onSave}
- isSaving={isSaving}
- saveError={saveError}
- clearSaveError={clearSaveError}
- isLegacy={isLegacy}
- history={history as ScopedHistory}
- />
-
-
+
+
+ ) : (
+
+ )
+ }
+ onSave={onSave}
+ isSaving={isSaving}
+ saveError={saveError}
+ clearSaveError={clearSaveError}
+ isLegacy={isLegacy}
+ history={history as ScopedHistory}
+ />
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
index f4ffe97931a24..ff6909d4666f8 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
@@ -7,16 +7,17 @@
import React, { useEffect, useState, Fragment } from 'react';
import { RouteComponentProps } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui';
+import { EuiPageContentBody, EuiSpacer, EuiCallOut } from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import { TemplateDeserialized } from '../../../../common';
-import { attemptToURIDecode } from '../../../shared_imports';
+import { PageError, PageLoading, attemptToURIDecode, Error } from '../../../shared_imports';
import { breadcrumbService } from '../../services/breadcrumbs';
import { useLoadIndexTemplate, updateTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
-import { SectionLoading, SectionError, TemplateForm, Error } from '../../components';
+import { TemplateForm } from '../../components';
import { getIsLegacyFromQueryParams } from '../../lib/index_templates';
interface MatchParams {
@@ -62,27 +63,27 @@ export const TemplateEdit: React.FunctionComponent
+ return (
+
-
+
);
} else if (error) {
- content = (
-
}
- error={error as Error}
+ error={error}
data-test-subj="sectionError"
/>
);
@@ -91,80 +92,75 @@ export const TemplateEdit: React.FunctionComponent
}
- color="danger"
- iconType="alert"
+ error={
+ {
+ message: i18n.translate(
+ 'xpack.idxMgmt.templateEdit.managedTemplateWarningDescription',
+ {
+ defaultMessage: 'Managed templates are critical for internal operations.',
+ }
+ ),
+ } as Error
+ }
data-test-subj="systemTemplateEditCallout"
- >
-
-
+ />
);
- } else {
- content = (
+ }
+ }
+
+ return (
+
+ {isSystemTemplate && (
- {isSystemTemplate && (
-
-
- }
- color="danger"
- iconType="alert"
- data-test-subj="systemTemplateEditCallout"
- >
-
-
-
-
- )}
-
-
-
-
-
+
}
- defaultValue={template}
- onSave={onSave}
- isSaving={isSaving}
- saveError={saveError}
- clearSaveError={clearSaveError}
- isEditing={true}
- isLegacy={isLegacy}
- history={history as ScopedHistory}
- />
+ color="danger"
+ iconType="alert"
+ data-test-subj="systemTemplateEditCallout"
+ >
+
+
+
- );
- }
- }
+ )}
- return (
-
- {content}
-
+
+ }
+ defaultValue={template!}
+ onSave={onSave}
+ isSaving={isSaving}
+ saveError={saveError}
+ clearSaveError={clearSaveError}
+ isEditing={true}
+ isLegacy={isLegacy}
+ history={history as ScopedHistory}
+ />
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts b/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts
new file mode 100644
index 0000000000000..d60cd1cf8aabf
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useLocatorUrl } from '../../../../../../src/plugins/share/public';
+import { useAppContext } from '../app_context';
+import { ILM_LOCATOR_ID } from '../constants';
+
+export const useIlmLocator = (
+ page: 'policies_list' | 'policy_edit' | 'policy_create',
+ policyName?: string
+): string => {
+ const ctx = useAppContext();
+ const locator = policyName === undefined ? null : ctx.url.locators.get(ILM_LOCATOR_ID)!;
+ const url = useLocatorUrl(locator, { page, policyName }, {}, [page, policyName]);
+
+ return url;
+};
diff --git a/x-pack/plugins/index_management/public/application/services/use_request.ts b/x-pack/plugins/index_management/public/application/services/use_request.ts
index f4d3426439562..3b1d5cf22452d 100644
--- a/x-pack/plugins/index_management/public/application/services/use_request.ts
+++ b/x-pack/plugins/index_management/public/application/services/use_request.ts
@@ -11,6 +11,7 @@ import {
UseRequestConfig,
sendRequest as _sendRequest,
useRequest as _useRequest,
+ Error,
} from '../../shared_imports';
import { httpService } from './http';
@@ -19,6 +20,6 @@ export const sendRequest = (config: SendRequestConfig): Promise(config: UseRequestConfig) => {
- return _useRequest(httpService.httpClient, config);
+export const useRequest = (config: UseRequestConfig) => {
+ return _useRequest(httpService.httpClient, config);
};
diff --git a/x-pack/plugins/index_management/public/application/services/use_url_generator.ts b/x-pack/plugins/index_management/public/application/services/use_url_generator.ts
deleted file mode 100644
index 2d9ab3959d769..0000000000000
--- a/x-pack/plugins/index_management/public/application/services/use_url_generator.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { useEffect, useState } from 'react';
-import {
- UrlGeneratorContract,
- UrlGeneratorId,
- UrlGeneratorStateMapping,
-} from '../../../../../../src/plugins/share/public';
-import { useAppContext } from '../app_context';
-
-export const useUrlGenerator = ({
- urlGeneratorId,
- urlGeneratorState,
-}: {
- urlGeneratorId: UrlGeneratorId;
- urlGeneratorState: UrlGeneratorStateMapping[UrlGeneratorId]['State'];
-}) => {
- const { urlGenerators } = useAppContext();
- const [link, setLink] = useState();
- useEffect(() => {
- const updateLink = async (): Promise => {
- let urlGenerator: UrlGeneratorContract;
- try {
- urlGenerator = urlGenerators.getUrlGenerator(urlGeneratorId);
- const url = await urlGenerator.createUrl(urlGeneratorState);
- setLink(url);
- } catch (e) {
- // do nothing
- }
- };
-
- updateLink();
- }, [urlGeneratorId, urlGeneratorState, urlGenerators]);
- return link;
-};
diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts
index eddac8e4b8a86..fa27b22e502fa 100644
--- a/x-pack/plugins/index_management/public/shared_imports.ts
+++ b/x-pack/plugins/index_management/public/shared_imports.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+export { APP_WRAPPER_CLASS } from '../../../../src/core/public';
+
export {
SendRequestConfig,
SendRequestResponse,
@@ -16,6 +18,10 @@ export {
extractQueryParams,
GlobalFlyout,
attemptToURIDecode,
+ PageLoading,
+ PageError,
+ Error,
+ SectionLoading,
} from '../../../../src/plugins/es_ui_shared/public';
export {
diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
index bd000186d91c4..231a2764d2710 100644
--- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
+++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
@@ -17,28 +17,40 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
-export function registerGetAllRoute({ router }: RouteDependencies) {
+export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDependencies) {
router.get({ path: addBasePath('/index_templates'), validate: false }, async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
- const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser);
- const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate');
- const { index_templates: templatesEs } = await callAsCurrentUser(
- 'dataManagement.getComposableIndexTemplates'
- );
-
- const legacyTemplates = deserializeLegacyTemplateList(
- legacyTemplatesEs,
- cloudManagedTemplatePrefix
- );
- const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix);
-
- const body = {
- templates,
- legacyTemplates,
- };
-
- return res.ok({ body });
+ try {
+ const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser);
+
+ const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate');
+ const { index_templates: templatesEs } = await callAsCurrentUser(
+ 'dataManagement.getComposableIndexTemplates'
+ );
+
+ const legacyTemplates = deserializeLegacyTemplateList(
+ legacyTemplatesEs,
+ cloudManagedTemplatePrefix
+ );
+ const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix);
+
+ const body = {
+ templates,
+ legacyTemplates,
+ };
+
+ return res.ok({ body });
+ } catch (error) {
+ if (isEsError(error)) {
+ return res.customError({
+ statusCode: error.statusCode,
+ body: error,
+ });
+ }
+ // Case: default
+ throw error;
+ }
});
}
diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
index 41867053c3a0f..c3327dc3fe85d 100644
--- a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
+++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
@@ -36,12 +36,12 @@ export const MetricsAlertDropdown = () => {
() => ({
id: 1,
title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', {
- defaultMessage: 'Infrastructure alerts',
+ defaultMessage: 'Infrastructure rules',
}),
items: [
{
name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', {
- defaultMessage: 'Create inventory alert',
+ defaultMessage: 'Create inventory rule',
}),
onClick: () => setVisibleFlyoutType('inventory'),
},
@@ -54,12 +54,12 @@ export const MetricsAlertDropdown = () => {
() => ({
id: 2,
title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', {
- defaultMessage: 'Metrics alerts',
+ defaultMessage: 'Metrics rules',
}),
items: [
{
name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', {
- defaultMessage: 'Create threshold alert',
+ defaultMessage: 'Create threshold rule',
}),
onClick: () => setVisibleFlyoutType('threshold'),
},
@@ -76,7 +76,7 @@ export const MetricsAlertDropdown = () => {
const manageAlertsMenuItem = useMemo(
() => ({
name: i18n.translate('xpack.infra.alerting.manageAlerts', {
- defaultMessage: 'Manage alerts',
+ defaultMessage: 'Manage rules',
}),
icon: 'tableOfContents',
onClick: manageAlertsLinkProps.onClick,
@@ -112,7 +112,7 @@ export const MetricsAlertDropdown = () => {
{
id: 0,
title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', {
- defaultMessage: 'Alerts',
+ defaultMessage: 'Alerts and rules',
}),
items: firstPanelMenuItems,
},
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx
index a6b69a37f780e..c9b6275264f91 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx
@@ -17,7 +17,7 @@ export const ManageAlertsContextMenuItem = () => {
});
return (
-
+
);
};
diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx
index 66c77fbf875a4..c1733d4af0589 100644
--- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx
+++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx
@@ -66,13 +66,13 @@ export const AlertDropdown = () => {
>
,
,
];
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx
index 94b16448a6b61..ea80bd13e8a4d 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx
@@ -25,6 +25,7 @@ import {
SectionSubtitle,
SectionLinks,
SectionLink,
+ ActionMenuDivider,
} from '../../../../../../../observability/public';
import { useLinkProps } from '../../../../../hooks/use_link_props';
@@ -173,7 +174,10 @@ export const NodeContextMenu: React.FC = withTheme
-
+
+
+
+
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
index cf3d8a15b7b65..922b10e8bd2b0 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
@@ -123,7 +123,7 @@ const getData = async (
const client = async (
options: CallWithRequestParams
): Promise> =>
- // @ts-expect-error @elastic/elasticsearch SearchResponse.body.timeout is not required
+ // @ts-expect-error SearchResponse.body.timeout is optional
(await esClient.search(options)).body as InfraDatabaseSearchResponse;
const metrics = [
diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts
index aa34204b9fb44..1f0f13eeb6ca9 100644
--- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts
+++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts
@@ -84,7 +84,7 @@ export const logEntrySearchStrategyProvider = ({
tiebreakerField,
runtimeMappings,
}): IEsSearchRequest => ({
- // @ts-expect-error @elastic/elasticsearch declares indices_boost as Record
+ // @ts-expect-error `Field` is not assignable to `SearchRequest.docvalue_fields`
params: createGetLogEntryQuery(
indices,
params.logEntryId,
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx
index e29bb2ac6e92e..b8c8f6c58f711 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx
@@ -54,9 +54,10 @@ describe('Processor: Circle', () => {
// Click submit button with only the type defined
await saveNewProcessor();
- // Expect form error as "field" and "shape_type" are required parameters
+ // Expect form error as "field", "shape_type" and "error_distance" are required parameters
expect(form.getErrorsMessages()).toEqual([
'A field value is required.',
+ 'An error distance value is required.',
'A shape type value is required.',
]);
});
@@ -91,15 +92,15 @@ describe('Processor: Circle', () => {
form,
} = testBed;
- // Add "field" value (required)
+ // Set required parameters
form.setInputValue('fieldNameField.input', 'field_1');
- // Select the shape
form.setSelectValue('shapeSelectorField', 'geo_shape');
- // Add "target_field" value
- form.setInputValue('targetField.input', 'target_field');
-
form.setInputValue('errorDistanceField.input', '10');
+ // Set optional parameters
+ form.setInputValue('targetField.input', 'target_field');
+ form.toggleEuiSwitch('ignoreMissingSwitch.input');
+
// Save the field with new changes
await saveNewProcessor();
@@ -109,6 +110,7 @@ describe('Processor: Circle', () => {
error_distance: 10,
shape_type: 'geo_shape',
target_field: 'target_field',
+ ignore_missing: true,
});
});
});
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx
index 74a7f37d841ae..87e08eaeea6e6 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx
@@ -13,6 +13,7 @@ import { EuiCode } from '@elastic/eui';
import {
FIELD_TYPES,
fieldValidators,
+ fieldFormatters,
UseField,
SelectField,
NumericField,
@@ -24,13 +25,13 @@ import { FieldNameField } from './common_fields/field_name_field';
import { TargetField } from './common_fields/target_field';
const { emptyField } = fieldValidators;
+const { toInt } = fieldFormatters;
const fieldsConfig: FieldsConfig = {
/* Required fields config */
error_distance: {
type: FIELD_TYPES.NUMBER,
- deserializer: (v) => (typeof v === 'number' && !isNaN(v) ? v : 1.0),
- serializer: Number,
+ formatters: [toInt],
label: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceFieldLabel',
{
@@ -49,18 +50,11 @@ const fieldsConfig: FieldsConfig = {
),
validations: [
{
- validator: ({ value }) => {
- return isNaN(Number(value))
- ? {
- message: i18n.translate(
- 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError',
- {
- defaultMessage: 'An error distance value is required.',
- }
- ),
- }
- : undefined;
- },
+ validator: emptyField(
+ i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError', {
+ defaultMessage: 'An error distance value is required.',
+ })
+ ),
},
],
},
@@ -110,14 +104,14 @@ export const Circle: FunctionComponent = () => {
options: [
{
value: 'shape',
- label: i18n.translate(
+ text: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeShape',
{ defaultMessage: 'Shape' }
),
},
{
value: 'geo_shape',
- label: i18n.translate(
+ text: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeGeoShape',
{ defaultMessage: 'Geo-shape' }
),
diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts
index 8948a3e8d56be..d120f60ef8a2d 100644
--- a/x-pack/plugins/ingest_pipelines/public/index.ts
+++ b/x-pack/plugins/ingest_pipelines/public/index.ts
@@ -10,10 +10,3 @@ import { IngestPipelinesPlugin } from './plugin';
export function plugin() {
return new IngestPipelinesPlugin();
}
-
-export {
- INGEST_PIPELINES_APP_ULR_GENERATOR,
- IngestPipelinesUrlGenerator,
- IngestPipelinesUrlGeneratorState,
- INGEST_PIPELINES_PAGES,
-} from './url_generator';
diff --git a/x-pack/plugins/ingest_pipelines/public/locator.test.ts b/x-pack/plugins/ingest_pipelines/public/locator.test.ts
new file mode 100644
index 0000000000000..0b1246b2bed59
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/locator.test.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ManagementAppLocatorDefinition } from 'src/plugins/management/common/locator';
+import { IngestPipelinesLocatorDefinition, INGEST_PIPELINES_PAGES } from './locator';
+
+describe('Ingest pipeline locator', () => {
+ const setup = () => {
+ const managementDefinition = new ManagementAppLocatorDefinition();
+ const definition = new IngestPipelinesLocatorDefinition({
+ managementAppLocator: {
+ getLocation: (params) => managementDefinition.getLocation(params),
+ getUrl: async () => {
+ throw new Error('not implemented');
+ },
+ navigate: async () => {
+ throw new Error('not implemented');
+ },
+ useUrl: () => '',
+ },
+ });
+ return { definition };
+ };
+
+ describe('Pipelines List', () => {
+ it('generates relative url for list without pipelineId', async () => {
+ const { definition } = setup();
+ const location = await definition.getLocation({
+ page: INGEST_PIPELINES_PAGES.LIST,
+ });
+
+ expect(location).toMatchObject({
+ app: 'management',
+ path: '/ingest/ingest_pipelines',
+ });
+ });
+
+ it('generates relative url for list with a pipelineId', async () => {
+ const { definition } = setup();
+ const location = await definition.getLocation({
+ page: INGEST_PIPELINES_PAGES.LIST,
+ pipelineId: 'pipeline_name',
+ });
+
+ expect(location).toMatchObject({
+ app: 'management',
+ path: '/ingest/ingest_pipelines/?pipeline=pipeline_name',
+ });
+ });
+ });
+
+ describe('Pipeline Edit', () => {
+ it('generates relative url for pipeline edit', async () => {
+ const { definition } = setup();
+ const location = await definition.getLocation({
+ page: INGEST_PIPELINES_PAGES.EDIT,
+ pipelineId: 'pipeline_name',
+ });
+
+ expect(location).toMatchObject({
+ app: 'management',
+ path: '/ingest/ingest_pipelines/edit/pipeline_name',
+ });
+ });
+ });
+
+ describe('Pipeline Clone', () => {
+ it('generates relative url for pipeline clone', async () => {
+ const { definition } = setup();
+ const location = await definition.getLocation({
+ page: INGEST_PIPELINES_PAGES.CLONE,
+ pipelineId: 'pipeline_name',
+ });
+
+ expect(location).toMatchObject({
+ app: 'management',
+ path: '/ingest/ingest_pipelines/create/pipeline_name',
+ });
+ });
+ });
+
+ describe('Pipeline Create', () => {
+ it('generates relative url for pipeline create', async () => {
+ const { definition } = setup();
+ const location = await definition.getLocation({
+ page: INGEST_PIPELINES_PAGES.CREATE,
+ pipelineId: 'pipeline_name',
+ });
+
+ expect(location).toMatchObject({
+ app: 'management',
+ path: '/ingest/ingest_pipelines/create',
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_pipelines/public/locator.ts b/x-pack/plugins/ingest_pipelines/public/locator.ts
new file mode 100644
index 0000000000000..d819011f14f47
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/locator.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SerializableState } from 'src/plugins/kibana_utils/common';
+import { ManagementAppLocator } from 'src/plugins/management/common';
+import {
+ LocatorPublic,
+ LocatorDefinition,
+ KibanaLocation,
+} from '../../../../src/plugins/share/public';
+import {
+ getClonePath,
+ getCreatePath,
+ getEditPath,
+ getListPath,
+} from './application/services/navigation';
+import { PLUGIN_ID } from '../common/constants';
+
+export enum INGEST_PIPELINES_PAGES {
+ LIST = 'pipelines_list',
+ EDIT = 'pipeline_edit',
+ CREATE = 'pipeline_create',
+ CLONE = 'pipeline_clone',
+}
+
+interface IngestPipelinesBaseParams extends SerializableState {
+ pipelineId: string;
+}
+export interface IngestPipelinesListParams extends Partial {
+ page: INGEST_PIPELINES_PAGES.LIST;
+}
+
+export interface IngestPipelinesEditParams extends IngestPipelinesBaseParams {
+ page: INGEST_PIPELINES_PAGES.EDIT;
+}
+
+export interface IngestPipelinesCloneParams extends IngestPipelinesBaseParams {
+ page: INGEST_PIPELINES_PAGES.CLONE;
+}
+
+export interface IngestPipelinesCreateParams extends IngestPipelinesBaseParams {
+ page: INGEST_PIPELINES_PAGES.CREATE;
+}
+
+export type IngestPipelinesParams =
+ | IngestPipelinesListParams
+ | IngestPipelinesEditParams
+ | IngestPipelinesCloneParams
+ | IngestPipelinesCreateParams;
+
+export type IngestPipelinesLocator = LocatorPublic;
+
+export const INGEST_PIPELINES_APP_LOCATOR = 'INGEST_PIPELINES_APP_LOCATOR';
+
+export interface IngestPipelinesLocatorDependencies {
+ managementAppLocator: ManagementAppLocator;
+}
+
+export class IngestPipelinesLocatorDefinition implements LocatorDefinition {
+ public readonly id = INGEST_PIPELINES_APP_LOCATOR;
+
+ constructor(protected readonly deps: IngestPipelinesLocatorDependencies) {}
+
+ public readonly getLocation = async (params: IngestPipelinesParams): Promise => {
+ const location = await this.deps.managementAppLocator.getLocation({
+ sectionId: 'ingest',
+ appId: PLUGIN_ID,
+ });
+
+ let path: string = '';
+
+ switch (params.page) {
+ case INGEST_PIPELINES_PAGES.EDIT:
+ path = getEditPath({
+ pipelineName: params.pipelineId,
+ });
+ break;
+ case INGEST_PIPELINES_PAGES.CREATE:
+ path = getCreatePath();
+ break;
+ case INGEST_PIPELINES_PAGES.LIST:
+ path = getListPath({
+ inspectedPipelineName: params.pipelineId,
+ });
+ break;
+ case INGEST_PIPELINES_PAGES.CLONE:
+ path = getClonePath({
+ clonedPipelineName: params.pipelineId,
+ });
+ break;
+ }
+
+ return {
+ ...location,
+ path: path === '/' ? location.path : location.path + path,
+ };
+ };
+}
diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts
index 4a138a12d6819..b4eb33162a1f4 100644
--- a/x-pack/plugins/ingest_pipelines/public/plugin.ts
+++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts
@@ -11,7 +11,7 @@ import { CoreSetup, Plugin } from 'src/core/public';
import { PLUGIN_ID } from '../common/constants';
import { uiMetricService, apiService } from './application/services';
import { SetupDependencies, StartDependencies } from './types';
-import { registerUrlGenerator } from './url_generator';
+import { IngestPipelinesLocatorDefinition } from './locator';
export class IngestPipelinesPlugin
implements Plugin {
@@ -50,7 +50,11 @@ export class IngestPipelinesPlugin
},
});
- registerUrlGenerator(coreSetup, management, share);
+ share.url.locators.create(
+ new IngestPipelinesLocatorDefinition({
+ managementAppLocator: management.locator,
+ })
+ );
}
public start() {}
diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts
deleted file mode 100644
index dc45f9bc39088..0000000000000
--- a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { IngestPipelinesUrlGenerator, INGEST_PIPELINES_PAGES } from './url_generator';
-
-describe('IngestPipelinesUrlGenerator', () => {
- const getAppBasePath = (absolute: boolean = false) => {
- if (absolute) {
- return Promise.resolve('http://localhost/app/test_app');
- }
- return Promise.resolve('/app/test_app');
- };
- const urlGenerator = new IngestPipelinesUrlGenerator(getAppBasePath);
-
- describe('Pipelines List', () => {
- it('generates relative url for list without pipelineId', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.LIST,
- });
- expect(url).toBe('/app/test_app/');
- });
-
- it('generates absolute url for list without pipelineId', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.LIST,
- absolute: true,
- });
- expect(url).toBe('http://localhost/app/test_app/');
- });
- it('generates relative url for list with a pipelineId', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.LIST,
- pipelineId: 'pipeline_name',
- });
- expect(url).toBe('/app/test_app/?pipeline=pipeline_name');
- });
-
- it('generates absolute url for list with a pipelineId', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.LIST,
- pipelineId: 'pipeline_name',
- absolute: true,
- });
- expect(url).toBe('http://localhost/app/test_app/?pipeline=pipeline_name');
- });
- });
-
- describe('Pipeline Edit', () => {
- it('generates relative url for pipeline edit', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.EDIT,
- pipelineId: 'pipeline_name',
- });
- expect(url).toBe('/app/test_app/edit/pipeline_name');
- });
-
- it('generates absolute url for pipeline edit', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.EDIT,
- pipelineId: 'pipeline_name',
- absolute: true,
- });
- expect(url).toBe('http://localhost/app/test_app/edit/pipeline_name');
- });
- });
-
- describe('Pipeline Clone', () => {
- it('generates relative url for pipeline clone', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.CLONE,
- pipelineId: 'pipeline_name',
- });
- expect(url).toBe('/app/test_app/create/pipeline_name');
- });
-
- it('generates absolute url for pipeline clone', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.CLONE,
- pipelineId: 'pipeline_name',
- absolute: true,
- });
- expect(url).toBe('http://localhost/app/test_app/create/pipeline_name');
- });
- });
-
- describe('Pipeline Create', () => {
- it('generates relative url for pipeline create', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.CREATE,
- pipelineId: 'pipeline_name',
- });
- expect(url).toBe('/app/test_app/create');
- });
-
- it('generates absolute url for pipeline create', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.CREATE,
- pipelineId: 'pipeline_name',
- absolute: true,
- });
- expect(url).toBe('http://localhost/app/test_app/create');
- });
- });
-});
diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.ts
deleted file mode 100644
index d9a77addcd5fd..0000000000000
--- a/x-pack/plugins/ingest_pipelines/public/url_generator.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { CoreSetup } from 'src/core/public';
-import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public';
-import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public';
-import {
- getClonePath,
- getCreatePath,
- getEditPath,
- getListPath,
-} from './application/services/navigation';
-import { SetupDependencies } from './types';
-import { PLUGIN_ID } from '../common/constants';
-
-export const INGEST_PIPELINES_APP_ULR_GENERATOR = 'INGEST_PIPELINES_APP_URL_GENERATOR';
-
-export enum INGEST_PIPELINES_PAGES {
- LIST = 'pipelines_list',
- EDIT = 'pipeline_edit',
- CREATE = 'pipeline_create',
- CLONE = 'pipeline_clone',
-}
-
-interface UrlGeneratorState {
- pipelineId: string;
- absolute?: boolean;
-}
-export interface PipelinesListUrlGeneratorState extends Partial {
- page: INGEST_PIPELINES_PAGES.LIST;
-}
-
-export interface PipelineEditUrlGeneratorState extends UrlGeneratorState {
- page: INGEST_PIPELINES_PAGES.EDIT;
-}
-
-export interface PipelineCloneUrlGeneratorState extends UrlGeneratorState {
- page: INGEST_PIPELINES_PAGES.CLONE;
-}
-
-export interface PipelineCreateUrlGeneratorState extends UrlGeneratorState {
- page: INGEST_PIPELINES_PAGES.CREATE;
-}
-
-export type IngestPipelinesUrlGeneratorState =
- | PipelinesListUrlGeneratorState
- | PipelineEditUrlGeneratorState
- | PipelineCloneUrlGeneratorState
- | PipelineCreateUrlGeneratorState;
-
-export class IngestPipelinesUrlGenerator
- implements UrlGeneratorsDefinition {
- constructor(private readonly getAppBasePath: (absolute: boolean) => Promise) {}
-
- public readonly id = INGEST_PIPELINES_APP_ULR_GENERATOR;
-
- public readonly createUrl = async (state: IngestPipelinesUrlGeneratorState): Promise => {
- switch (state.page) {
- case INGEST_PIPELINES_PAGES.EDIT: {
- return `${await this.getAppBasePath(!!state.absolute)}${getEditPath({
- pipelineName: state.pipelineId,
- })}`;
- }
- case INGEST_PIPELINES_PAGES.CREATE: {
- return `${await this.getAppBasePath(!!state.absolute)}${getCreatePath()}`;
- }
- case INGEST_PIPELINES_PAGES.LIST: {
- return `${await this.getAppBasePath(!!state.absolute)}${getListPath({
- inspectedPipelineName: state.pipelineId,
- })}`;
- }
- case INGEST_PIPELINES_PAGES.CLONE: {
- return `${await this.getAppBasePath(!!state.absolute)}${getClonePath({
- clonedPipelineName: state.pipelineId,
- })}`;
- }
- }
- };
-}
-
-export const registerUrlGenerator = (
- coreSetup: CoreSetup,
- management: SetupDependencies['management'],
- share: SetupDependencies['share']
-) => {
- const getAppBasePath = async (absolute = false) => {
- const [coreStart] = await coreSetup.getStartServices();
- return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, {
- path: management.sections.section.ingest.getApp(PLUGIN_ID)!.basePath,
- absolute: !!absolute,
- });
- };
-
- share.urlGenerators.registerUrlGenerator(new IngestPipelinesUrlGenerator(getAppBasePath));
-};
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
index 972ef99d7d7f6..4a34bd030429e 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
@@ -6,7 +6,8 @@
*/
import React from 'react';
-import { shallow } from 'enzyme';
+import { ReactWrapper, shallow } from 'enzyme';
+import { act } from 'react-dom/test-utils';
import { mountWithIntl } from '@kbn/test/jest';
import { EuiDataGrid } from '@elastic/eui';
import { IAggType, IFieldFormat } from 'src/plugins/data/public';
@@ -83,6 +84,13 @@ function copyData(data: LensMultiTable): LensMultiTable {
return JSON.parse(JSON.stringify(data));
}
+async function waitForWrapperUpdate(wrapper: ReactWrapper) {
+ await act(async () => {
+ await new Promise((r) => setTimeout(r, 0));
+ });
+ wrapper.update();
+}
+
describe('DatatableComponent', () => {
let onDispatchEvent: jest.Mock;
@@ -149,7 +157,7 @@ describe('DatatableComponent', () => {
).toMatchSnapshot();
});
- test('it invokes executeTriggerActions with correct context on click on top value', () => {
+ test('it invokes executeTriggerActions with correct context on click on top value', async () => {
const { args, data } = sampleArgs();
const wrapper = mountWithIntl(
@@ -173,6 +181,8 @@ describe('DatatableComponent', () => {
wrapper.find('[data-test-subj="dataGridRowCell"]').first().simulate('focus');
+ await waitForWrapperUpdate(wrapper);
+
wrapper.find('[data-test-subj="lensDatatableFilterOut"]').first().simulate('click');
expect(onDispatchEvent).toHaveBeenCalledWith({
@@ -192,7 +202,7 @@ describe('DatatableComponent', () => {
});
});
- test('it invokes executeTriggerActions with correct context on click on timefield', () => {
+ test('it invokes executeTriggerActions with correct context on click on timefield', async () => {
const { args, data } = sampleArgs();
const wrapper = mountWithIntl(
@@ -216,6 +226,8 @@ describe('DatatableComponent', () => {
wrapper.find('[data-test-subj="dataGridRowCell"]').at(1).simulate('focus');
+ await waitForWrapperUpdate(wrapper);
+
wrapper.find('[data-test-subj="lensDatatableFilterFor"]').first().simulate('click');
expect(onDispatchEvent).toHaveBeenCalledWith({
@@ -235,7 +247,7 @@ describe('DatatableComponent', () => {
});
});
- test('it invokes executeTriggerActions with correct context on click on timefield from range', () => {
+ test('it invokes executeTriggerActions with correct context on click on timefield from range', async () => {
const data: LensMultiTable = {
type: 'lens_multitable',
tables: {
@@ -298,6 +310,8 @@ describe('DatatableComponent', () => {
wrapper.find('[data-test-subj="dataGridRowCell"]').at(0).simulate('focus');
+ await waitForWrapperUpdate(wrapper);
+
wrapper.find('[data-test-subj="lensDatatableFilterFor"]').first().simulate('click');
expect(onDispatchEvent).toHaveBeenCalledWith({
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
index 2f3eb5043d610..c62b10093e6e5 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
@@ -113,7 +113,7 @@ export function DimensionContainer({
>
{i18n.translate('xpack.lens.configure.configurePanelTitle', {
- defaultMessage: '{groupLabel} configuration',
+ defaultMessage: '{groupLabel}',
values: {
groupLabel,
},
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
index 52488cb32ae83..0e2ba5ce8ad59 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
@@ -1370,6 +1370,57 @@ describe('editor_frame', () => {
})
);
});
+
+ it('should avoid completely to compute suggestion when in fullscreen mode', async () => {
+ const props = {
+ ...getDefaultProps(),
+ initialContext: {
+ indexPatternId: '1',
+ fieldName: 'test',
+ },
+ visualizationMap: {
+ testVis: mockVisualization,
+ },
+ datasourceMap: {
+ testDatasource: mockDatasource,
+ testDatasource2: mockDatasource2,
+ },
+
+ ExpressionRenderer: expressionRendererMock,
+ };
+
+ const { instance: el } = await mountWithProvider(
+ ,
+ props.plugins.data
+ );
+ instance = el;
+
+ expect(
+ instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement
+ ).not.toBeUndefined();
+
+ await act(async () => {
+ (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
+ type: 'TOGGLE_FULLSCREEN',
+ });
+ });
+
+ instance.update();
+
+ expect(instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement).toBe(false);
+
+ await act(async () => {
+ (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
+ type: 'TOGGLE_FULLSCREEN',
+ });
+ });
+
+ instance.update();
+
+ expect(
+ instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement
+ ).not.toBeUndefined();
+ });
});
describe('passing state back to the caller', () => {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
index cc65bb126d2d9..bd96682f427fa 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
@@ -452,7 +452,8 @@ export function EditorFrame(props: EditorFrameProps) {
)
}
suggestionsPanel={
- allLoaded && (
+ allLoaded &&
+ !state.isFullscreenDatasource && (
{
const parsedValue = parseTimeShift(value);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
index 03b9d6c07709c..87116f71919b5 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
@@ -7,11 +7,12 @@
import { i18n } from '@kbn/i18n';
import type { ExpressionFunctionAST } from '@kbn/interpreter/common';
+import memoizeOne from 'memoize-one';
import type { TimeScaleUnit } from '../../../time_scale';
import type { IndexPattern, IndexPatternLayer } from '../../../types';
import { adjustTimeScaleLabelSuffix } from '../../time_scale_utils';
import type { ReferenceBasedIndexPatternColumn } from '../column_types';
-import { isColumnValidAsReference } from '../../layer_helpers';
+import { getManagedColumnsFrom, isColumnValidAsReference } from '../../layer_helpers';
import { operationDefinitionMap } from '..';
export const buildLabelFunction = (ofName: (name?: string) => string) => (
@@ -45,6 +46,23 @@ export function checkForDateHistogram(layer: IndexPatternLayer, name: string) {
];
}
+const getFullyManagedColumnIds = memoizeOne((layer: IndexPatternLayer) => {
+ const managedColumnIds = new Set();
+ Object.entries(layer.columns).forEach(([id, column]) => {
+ if (
+ 'references' in column &&
+ operationDefinitionMap[column.operationType].input === 'managedReference'
+ ) {
+ managedColumnIds.add(id);
+ const managedColumns = getManagedColumnsFrom(id, layer.columns);
+ managedColumns.map(([managedId]) => {
+ managedColumnIds.add(managedId);
+ });
+ }
+ });
+ return managedColumnIds;
+});
+
export function checkReferences(layer: IndexPatternLayer, columnId: string) {
const column = layer.columns[columnId] as ReferenceBasedIndexPatternColumn;
@@ -72,7 +90,8 @@ export function checkReferences(layer: IndexPatternLayer, columnId: string) {
column: referenceColumn,
});
- if (!isValid) {
+ // do not enforce column validity if current column is part of managed subtree
+ if (!isValid && !getFullyManagedColumnIds(layer).has(columnId)) {
errors.push(
i18n.translate('xpack.lens.indexPattern.invalidReferenceConfiguration', {
defaultMessage: 'Dimension "{dimensionLabel}" is configured incorrectly',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
index 654a93374703d..d1b0ec8876feb 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
@@ -29,7 +29,7 @@ import { ParamEditorProps } from '../../index';
import { getManagedColumnsFrom } from '../../../layer_helpers';
import { ErrorWrapper, runASTValidation, tryToParse } from '../validation';
import {
- LensMathSuggestion,
+ LensMathSuggestions,
SUGGESTION_TYPE,
suggest,
getSuggestion,
@@ -329,7 +329,7 @@ export function FormulaEditor({
context: monaco.languages.CompletionContext
) => {
const innerText = model.getValue();
- let aSuggestions: { list: LensMathSuggestion[]; type: SUGGESTION_TYPE } = {
+ let aSuggestions: LensMathSuggestions = {
list: [],
type: SUGGESTION_TYPE.FIELD,
};
@@ -367,7 +367,13 @@ export function FormulaEditor({
return {
suggestions: aSuggestions.list.map((s) =>
- getSuggestion(s, aSuggestions.type, visibleOperationsMap, context.triggerCharacter)
+ getSuggestion(
+ s,
+ aSuggestions.type,
+ visibleOperationsMap,
+ context.triggerCharacter,
+ aSuggestions.range
+ )
),
};
},
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
index 9cd748f5759c9..c55f22dd682d0 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
@@ -18,6 +18,7 @@ import {
getHover,
suggest,
monacoPositionToOffset,
+ offsetToRowColumn,
getInfoAtZeroIndexedPosition,
} from './math_completion';
@@ -363,6 +364,36 @@ describe('math completion', () => {
});
});
+ describe('offsetToRowColumn', () => {
+ it('should work with single-line strings', () => {
+ const input = `0123456`;
+ expect(offsetToRowColumn(input, 5)).toEqual(
+ expect.objectContaining({
+ lineNumber: 1,
+ column: 6,
+ })
+ );
+ });
+
+ it('should work with multi-line strings accounting for newline characters', () => {
+ const input = `012
+456
+89')`;
+ expect(offsetToRowColumn(input, 0)).toEqual(
+ expect.objectContaining({
+ lineNumber: 1,
+ column: 1,
+ })
+ );
+ expect(offsetToRowColumn(input, 9)).toEqual(
+ expect.objectContaining({
+ lineNumber: 3,
+ column: 2,
+ })
+ );
+ });
+ });
+
describe('monacoPositionToOffset', () => {
it('should work with multi-line strings accounting for newline characters', () => {
const input = `012
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts
index 815df943cdba3..28e762e7dff0f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts
@@ -13,6 +13,7 @@ import {
TinymathLocation,
TinymathAST,
TinymathFunction,
+ TinymathVariable,
TinymathNamedArgument,
} from '@kbn/tinymath';
import type {
@@ -21,7 +22,7 @@ import type {
} from '../../../../../../../../../src/plugins/data/public';
import { IndexPattern } from '../../../../types';
import { memoizedGetAvailableOperationsByMetadata } from '../../../operations';
-import { tinymathFunctions, groupArgsByType } from '../util';
+import { tinymathFunctions, groupArgsByType, unquotedStringRegex } from '../util';
import type { GenericOperationDefinition } from '../..';
import { getFunctionSignatureLabel, getHelpTextContent } from './formula_help';
import { hasFunctionFieldArgument } from '../validation';
@@ -47,6 +48,7 @@ export type LensMathSuggestion =
export interface LensMathSuggestions {
list: LensMathSuggestion[];
type: SUGGESTION_TYPE;
+ range?: monaco.IRange;
}
function inLocation(cursorPosition: number, location: TinymathLocation) {
@@ -92,7 +94,7 @@ export function offsetToRowColumn(expression: string, offset: number): monaco.Po
let lineNumber = 1;
for (const line of lines) {
if (line.length >= remainingChars) {
- return new monaco.Position(lineNumber, remainingChars);
+ return new monaco.Position(lineNumber, remainingChars + 1);
}
remainingChars -= line.length + 1;
lineNumber++;
@@ -128,7 +130,7 @@ export async function suggest({
operationDefinitionMap: Record;
data: DataPublicPluginStart;
dateHistogramInterval?: number;
-}): Promise<{ list: LensMathSuggestion[]; type: SUGGESTION_TYPE }> {
+}): Promise {
const text =
expression.substr(0, zeroIndexedOffset) + MARKER + expression.substr(zeroIndexedOffset);
try {
@@ -154,6 +156,7 @@ export async function suggest({
return getArgumentSuggestions(
tokenInfo.parent,
tokenInfo.parent.args.findIndex((a) => a === tokenAst),
+ text,
indexPattern,
operationDefinitionMap
);
@@ -210,6 +213,7 @@ function getFunctionSuggestions(
function getArgumentSuggestions(
ast: TinymathFunction,
position: number,
+ expression: string,
indexPattern: IndexPattern,
operationDefinitionMap: Record
) {
@@ -280,7 +284,16 @@ function getArgumentSuggestions(
.filter((op) => op.operationType === operation.type)
.map((op) => ('field' in op ? op.field : undefined))
.filter((field) => field);
- return { list: fields as string[], type: SUGGESTION_TYPE.FIELD };
+ const fieldArg = ast.args[0];
+ const location = typeof fieldArg !== 'string' && (fieldArg as TinymathVariable).location;
+ let range: monaco.IRange | undefined;
+ if (location) {
+ const start = offsetToRowColumn(expression, location.min);
+ // This accounts for any characters that the user has already typed
+ const end = offsetToRowColumn(expression, location.max - MARKER.length);
+ range = monaco.Range.fromPositions(start, end);
+ }
+ return { list: fields as string[], type: SUGGESTION_TYPE.FIELD, range };
} else {
return { list: [], type: SUGGESTION_TYPE.FIELD };
}
@@ -375,7 +388,8 @@ export function getSuggestion(
suggestion: LensMathSuggestion,
type: SUGGESTION_TYPE,
operationDefinitionMap: Record,
- triggerChar: string | undefined
+ triggerChar: string | undefined,
+ range?: monaco.IRange
): monaco.languages.CompletionItem {
let kind: monaco.languages.CompletionItemKind = monaco.languages.CompletionItemKind.Method;
let label: string =
@@ -397,6 +411,10 @@ export function getSuggestion(
break;
case SUGGESTION_TYPE.FIELD:
kind = monaco.languages.CompletionItemKind.Value;
+ // Look for unsafe characters
+ if (unquotedStringRegex.test(label)) {
+ insertText = `'${label.replaceAll(`'`, "\\'")}'`;
+ }
break;
case SUGGESTION_TYPE.FUNCTIONS:
insertText = `${label}($0)`;
@@ -450,7 +468,7 @@ export function getSuggestion(
command,
additionalTextEdits: [],
// @ts-expect-error Monaco says this type is required, but provides a default value
- range: undefined,
+ range,
sortText,
filterText,
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
index e6aa29ea4d763..279e76b839548 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
@@ -413,13 +413,13 @@ describe('formula', () => {
).newLayer
).toEqual({
...layer,
- columnOrder: ['col1X0', 'col1X1', 'col1'],
+ columnOrder: ['col1X0', 'col1'],
columns: {
...layer.columns,
col1: {
...currentColumn,
label: 'average(bytes)',
- references: ['col1X1'],
+ references: ['col1X0'],
params: {
...currentColumn.params,
formula: 'average(bytes)',
@@ -436,18 +436,6 @@ describe('formula', () => {
sourceField: 'bytes',
timeScale: false,
},
- col1X1: {
- customLabel: true,
- dataType: 'number',
- isBucketed: false,
- label: 'Part of average(bytes)',
- operationType: 'math',
- params: {
- tinymathAst: 'col1X0',
- },
- references: ['col1X0'],
- scale: 'ratio',
- },
},
});
});
@@ -568,8 +556,8 @@ describe('formula', () => {
).locations
).toEqual({
col1X0: { min: 15, max: 29 },
- col1X2: { min: 0, max: 41 },
- col1X3: { min: 42, max: 50 },
+ col1X1: { min: 0, max: 41 },
+ col1X2: { min: 42, max: 50 },
});
});
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts
index a5c19c537acee..589f547434b91 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts
@@ -13,6 +13,7 @@ import {
} from '../index';
import { ReferenceBasedIndexPatternColumn } from '../column_types';
import { IndexPatternLayer } from '../../../types';
+import { unquotedStringRegex } from './util';
// Just handle two levels for now
type OperationParams = Record>;
@@ -25,6 +26,9 @@ export function getSafeFieldName({
if (!fieldName || operationType === 'count') {
return '';
}
+ if (unquotedStringRegex.test(fieldName)) {
+ return `'${fieldName.replaceAll(`'`, "\\'")}'`;
+ }
return fieldName;
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx
index 52522a18604aa..7aae35f496923 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx
@@ -48,7 +48,7 @@ export const mathOperation: OperationDefinition value);
- mathColumn.params.tinymathAst = consumedParam!;
- columns.push({ column: mathColumn });
- mathColumn.customLabel = true;
- mathColumn.label = label;
+ if (hasActualMathContent) {
+ const subNodeVariables = consumedParam ? findVariables(consumedParam) : [];
+ const mathColumn = mathOperation.buildColumn({
+ layer,
+ indexPattern,
+ });
+ mathColumn.references = subNodeVariables.map(({ value }) => value);
+ mathColumn.params.tinymathAst = consumedParam!;
+ columns.push({ column: mathColumn });
+ mathColumn.customLabel = true;
+ mathColumn.label = label;
+ }
const mappedParams = getOperationParams(nodeOperation, namedArguments || []);
const newCol = (nodeOperation as OperationDefinition<
@@ -143,7 +146,11 @@ function extractColumns(
{
layer,
indexPattern,
- referenceIds: [getManagedId(idPrefix, columns.length - 1)],
+ referenceIds: [
+ hasActualMathContent
+ ? getManagedId(idPrefix, columns.length - 1)
+ : (consumedParam as string),
+ ],
},
mappedParams
);
@@ -160,16 +167,19 @@ function extractColumns(
if (root === undefined) {
return [];
}
- const variables = findVariables(root);
- const mathColumn = mathOperation.buildColumn({
- layer,
- indexPattern,
- });
- mathColumn.references = variables.map(({ value }) => value);
- mathColumn.params.tinymathAst = root!;
- mathColumn.customLabel = true;
- mathColumn.label = label;
- columns.push({ column: mathColumn });
+ const topLevelMath = typeof root !== 'string';
+ if (topLevelMath) {
+ const variables = findVariables(root);
+ const mathColumn = mathOperation.buildColumn({
+ layer,
+ indexPattern,
+ });
+ mathColumn.references = variables.map(({ value }) => value);
+ mathColumn.params.tinymathAst = root!;
+ mathColumn.customLabel = true;
+ mathColumn.label = label;
+ columns.push({ column: mathColumn });
+ }
return columns;
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts
index d29682eafa329..9806cdaad637e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts
@@ -16,6 +16,8 @@ import type {
import type { OperationDefinition, IndexPatternColumn, GenericOperationDefinition } from '../index';
import type { GroupedNodes } from './types';
+export const unquotedStringRegex = /[^0-9A-Za-z._@\[\]/]/;
+
export function groupArgsByType(args: TinymathAST[]) {
const { namedArgument, variable, function: functions } = groupBy(
args,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx
index 7551b88039182..a458a1edcfa16 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx
@@ -424,25 +424,9 @@ export const termsOperation: OperationDefinition
- {i18n.translate('xpack.lens.indexPattern.terms.orderDirection', {
- defaultMessage: 'Rank direction',
- })}{' '}
-
- >
- }
+ label={i18n.translate('xpack.lens.indexPattern.terms.orderDirection', {
+ defaultMessage: 'Rank direction',
+ })}
display="columnCompressed"
fullWidth
>
@@ -513,7 +497,10 @@ export const termsOperation: OperationDefinition
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
index 3b557461546ca..f326f3e3ed5f6 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
@@ -60,7 +60,7 @@ describe('terms', () => {
size: 3,
orderDirection: 'asc',
},
- sourceField: 'category',
+ sourceField: 'source',
},
col2: {
label: 'Count',
@@ -88,7 +88,7 @@ describe('terms', () => {
expect.objectContaining({
arguments: expect.objectContaining({
orderBy: ['_key'],
- field: ['category'],
+ field: ['source'],
size: [3],
otherBucket: [true],
}),
@@ -770,6 +770,34 @@ describe('terms', () => {
expect(select.prop('disabled')).toEqual(false);
});
+ it('should disable missing bucket setting if field is not a string', () => {
+ const updateLayerSpy = jest.fn();
+ const instance = shallow(
+
+ );
+
+ const select = instance
+ .find('[data-test-subj="indexPattern-terms-missing-bucket"]')
+ .find(EuiSwitch);
+
+ expect(select.prop('disabled')).toEqual(true);
+ });
+
it('should update state when clicking other bucket toggle', () => {
const updateLayerSpy = jest.fn();
const instance = shallow(
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
index 34765217da664..7de1318cbac61 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
@@ -25,6 +25,7 @@ import { documentField } from '../document_field';
import { getFieldByNameFactory } from '../pure_helpers';
import { generateId } from '../../id_generator';
import { createMockedFullReference, createMockedManagedReference } from './mocks';
+import { TinymathAST } from 'packages/kbn-tinymath';
jest.mock('../operations');
jest.mock('../../id_generator');
@@ -105,28 +106,34 @@ describe('state_helpers', () => {
const source = {
dataType: 'number' as const,
isBucketed: false,
- label: 'moving_average(sum(bytes), window=5)',
+ label: '5 + moving_average(sum(bytes), window=5)',
operationType: 'formula' as const,
params: {
- formula: 'moving_average(sum(bytes), window=5)',
+ formula: '5 + moving_average(sum(bytes), window=5)',
isFormulaBroken: false,
},
- references: ['formulaX1'],
+ references: ['formulaX2'],
};
const math = {
customLabel: true,
dataType: 'number' as const,
isBucketed: false,
- label: 'Part of moving_average(sum(bytes), window=5)',
operationType: 'math' as const,
- params: { tinymathAst: 'formulaX2' },
- references: ['formulaX2'],
+ label: 'Part of 5 + moving_average(sum(bytes), window=5)',
+ references: ['formulaX1'],
+ params: {
+ tinymathAst: {
+ type: 'function',
+ name: 'add',
+ args: [5, 'formulaX1'],
+ } as TinymathAST,
+ },
};
const sum = {
customLabel: true,
dataType: 'number' as const,
isBucketed: false,
- label: 'Part of moving_average(sum(bytes), window=5)',
+ label: 'Part of 5 + moving_average(sum(bytes), window=5)',
operationType: 'sum' as const,
scale: 'ratio' as const,
sourceField: 'bytes',
@@ -135,7 +142,7 @@ describe('state_helpers', () => {
customLabel: true,
dataType: 'number' as const,
isBucketed: false,
- label: 'Part of moving_average(sum(bytes), window=5)',
+ label: 'Part of 5 + moving_average(sum(bytes), window=5)',
operationType: 'moving_average' as const,
params: { window: 5 },
references: ['formulaX0'],
@@ -148,14 +155,8 @@ describe('state_helpers', () => {
columns: {
source,
formulaX0: sum,
- formulaX1: math,
- formulaX2: movingAvg,
- formulaX3: {
- ...math,
- label: 'Part of moving_average(sum(bytes), window=5)',
- references: ['formulaX2'],
- params: { tinymathAst: 'formulaX2' },
- },
+ formulaX1: movingAvg,
+ formulaX2: math,
},
},
targetId: 'copy',
@@ -171,40 +172,34 @@ describe('state_helpers', () => {
'formulaX0',
'formulaX1',
'formulaX2',
- 'formulaX3',
'copyX0',
'copyX1',
'copyX2',
- 'copyX3',
'copy',
],
columns: {
source,
formulaX0: sum,
- formulaX1: math,
- formulaX2: movingAvg,
- formulaX3: {
- ...math,
- references: ['formulaX2'],
- params: { tinymathAst: 'formulaX2' },
- },
- copy: expect.objectContaining({ ...source, references: ['copyX3'] }),
+ formulaX1: movingAvg,
+ formulaX2: math,
+ copy: expect.objectContaining({ ...source, references: ['copyX2'] }),
copyX0: expect.objectContaining({
...sum,
}),
copyX1: expect.objectContaining({
- ...math,
+ ...movingAvg,
references: ['copyX0'],
- params: { tinymathAst: 'copyX0' },
}),
copyX2: expect.objectContaining({
- ...movingAvg,
- references: ['copyX1'],
- }),
- copyX3: expect.objectContaining({
...math,
- references: ['copyX2'],
- params: { tinymathAst: 'copyX2' },
+ references: ['copyX1'],
+ params: {
+ tinymathAst: expect.objectContaining({
+ type: 'function',
+ name: 'add',
+ args: [5, 'copyX1'],
+ } as TinymathAST),
+ },
}),
},
});
@@ -583,6 +578,46 @@ describe('state_helpers', () => {
).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] }));
});
+ it('should inherit filters from the incomplete column when passed', () => {
+ expect(
+ insertNewColumn({
+ layer: {
+ indexPatternId: '1',
+ columnOrder: ['col1'],
+ columns: {
+ col1: {
+ label: 'Date histogram of timestamp',
+ dataType: 'date',
+ isBucketed: true,
+
+ // Private
+ operationType: 'date_histogram',
+ sourceField: 'timestamp',
+ params: {
+ interval: 'h',
+ },
+ },
+ },
+ },
+ columnId: 'col2',
+ indexPattern,
+ op: 'average',
+ field: indexPattern.fields[2],
+ visualizationGroups: [],
+ incompleteParams: { filter: { language: 'kuery', query: '' }, timeShift: '3d' },
+ })
+ ).toEqual(
+ expect.objectContaining({
+ columns: expect.objectContaining({
+ col2: expect.objectContaining({
+ filter: { language: 'kuery', query: '' },
+ timeShift: '3d',
+ }),
+ }),
+ })
+ );
+ });
+
describe('inserting a new reference', () => {
it('should throw if the required references are impossible to match', () => {
// @ts-expect-error this function is not valid
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
index f0095b66e2ba6..fd3df9f97cecf 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
@@ -7,6 +7,7 @@
import { partition, mapValues, pickBy } from 'lodash';
import { CoreStart } from 'kibana/public';
+import { Query } from 'src/plugins/data/common';
import type {
FramePublicAPI,
OperationMetadata,
@@ -18,6 +19,7 @@ import {
OperationType,
IndexPatternColumn,
RequiredReference,
+ GenericOperationDefinition,
} from './definitions';
import type {
IndexPattern,
@@ -29,6 +31,13 @@ import { getSortScoreByPriority } from './operations';
import { generateId } from '../../id_generator';
import { ReferenceBasedIndexPatternColumn } from './definitions/column_types';
import { FormulaIndexPatternColumn, regenerateLayerFromAst } from './definitions/formula';
+import { TimeScaleUnit } from '../time_scale';
+
+interface ColumnAdvancedParams {
+ filter?: Query | undefined;
+ timeShift?: string | undefined;
+ timeScale?: TimeScaleUnit | undefined;
+}
interface ColumnChange {
op: OperationType;
@@ -39,6 +48,7 @@ interface ColumnChange {
visualizationGroups: VisualizationDimensionGroupConfig[];
targetGroup?: string;
shouldResetLabel?: boolean;
+ incompleteParams?: ColumnAdvancedParams;
}
interface ColumnCopy {
@@ -141,6 +151,24 @@ export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer {
return insertNewColumn(args);
}
+function ensureCompatibleParamsAreMoved(
+ column: T,
+ referencedOperation: GenericOperationDefinition,
+ previousColumn: ColumnAdvancedParams
+) {
+ const newColumn = { ...column };
+ if (referencedOperation.filterable) {
+ newColumn.filter = (previousColumn as ReferenceBasedIndexPatternColumn).filter;
+ }
+ if (referencedOperation.shiftable) {
+ newColumn.timeShift = (previousColumn as ReferenceBasedIndexPatternColumn).timeShift;
+ }
+ if (referencedOperation.timeScalingMode !== 'disabled') {
+ newColumn.timeScale = (previousColumn as ReferenceBasedIndexPatternColumn).timeScale;
+ }
+ return newColumn;
+}
+
// Insert a column into an empty ID. The field parameter is required when constructing
// a field-based operation, but will cause the function to fail for any other type of operation.
export function insertNewColumn({
@@ -152,6 +180,7 @@ export function insertNewColumn({
visualizationGroups,
targetGroup,
shouldResetLabel,
+ incompleteParams,
}: ColumnChange): IndexPatternLayer {
const operationDefinition = operationDefinitionMap[op];
@@ -163,7 +192,10 @@ export function insertNewColumn({
throw new Error(`Can't insert a column with an ID that is already in use`);
}
- const baseOptions = { indexPattern, previousColumn: layer.columns[columnId] };
+ const baseOptions = {
+ indexPattern,
+ previousColumn: { ...incompleteParams, ...layer.columns[columnId] },
+ };
if (operationDefinition.input === 'none' || operationDefinition.input === 'managedReference') {
if (field) {
@@ -414,15 +446,13 @@ export function replaceColumn({
indexPattern,
});
- const column = copyCustomLabel({ ...referenceColumn }, previousColumn);
// do not forget to move over also any filter/shift/anything (if compatible)
// from the reference definition to the new operation.
- if (referencedOperation.filterable) {
- column.filter = (previousColumn as ReferenceBasedIndexPatternColumn).filter;
- }
- if (referencedOperation.shiftable) {
- column.timeShift = (previousColumn as ReferenceBasedIndexPatternColumn).timeShift;
- }
+ const column = ensureCompatibleParamsAreMoved(
+ copyCustomLabel({ ...referenceColumn }, previousColumn),
+ referencedOperation,
+ previousColumn as ReferenceBasedIndexPatternColumn
+ );
tempLayer = {
...tempLayer,
@@ -529,15 +559,30 @@ export function replaceColumn({
}
if (!field) {
+ let incompleteColumn: {
+ operationType: OperationType;
+ } & ColumnAdvancedParams = { operationType: op };
// if no field is available perform a full clean of the column from the layer
if (previousDefinition.input === 'fullReference') {
tempLayer = deleteColumn({ layer: tempLayer, columnId, indexPattern });
+ const previousReferenceId = (previousColumn as ReferenceBasedIndexPatternColumn)
+ .references[0];
+ const referenceColumn = layer.columns[previousReferenceId];
+ if (referenceColumn) {
+ const referencedOperation = operationDefinitionMap[referenceColumn.operationType];
+
+ incompleteColumn = ensureCompatibleParamsAreMoved(
+ incompleteColumn,
+ referencedOperation,
+ previousColumn
+ );
+ }
}
return {
...tempLayer,
incompleteColumns: {
...(tempLayer.incompleteColumns ?? {}),
- [columnId]: { operationType: op },
+ [columnId]: incompleteColumn,
},
};
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx
index 14ba6b9189e6b..a1bc643c3bd93 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx
@@ -23,67 +23,67 @@ import { FramePublicAPI } from '../types';
export const timeShiftOptions = [
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.hour', {
- defaultMessage: '1 hour (1h)',
+ defaultMessage: '1 hour ago (1h)',
}),
value: '1h',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.3hours', {
- defaultMessage: '3 hours (3h)',
+ defaultMessage: '3 hours ago (3h)',
}),
value: '3h',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.6hours', {
- defaultMessage: '6 hours (6h)',
+ defaultMessage: '6 hours ago (6h)',
}),
value: '6h',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.12hours', {
- defaultMessage: '12 hours (12h)',
+ defaultMessage: '12 hours ago (12h)',
}),
value: '12h',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.day', {
- defaultMessage: '1 day (1d)',
+ defaultMessage: '1 day ago (1d)',
}),
value: '1d',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.week', {
- defaultMessage: '1 week (1w)',
+ defaultMessage: '1 week ago (1w)',
}),
value: '1w',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.month', {
- defaultMessage: '1 month (1M)',
+ defaultMessage: '1 month ago (1M)',
}),
value: '1M',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.3months', {
- defaultMessage: '3 months (3M)',
+ defaultMessage: '3 months ago (3M)',
}),
value: '3M',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.6months', {
- defaultMessage: '6 months (6M)',
+ defaultMessage: '6 months ago (6M)',
}),
value: '6M',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.year', {
- defaultMessage: '1 year (1y)',
+ defaultMessage: '1 year ago (1y)',
}),
value: '1y',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.previous', {
- defaultMessage: 'Previous',
+ defaultMessage: 'Previous time range',
}),
value: 'previous',
},
diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx
new file mode 100644
index 0000000000000..67e57dadd4935
--- /dev/null
+++ b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { LegendActionProps, SeriesIdentifier } from '@elastic/charts';
+import { EuiPopover } from '@elastic/eui';
+import { mountWithIntl } from '@kbn/test/jest';
+import { ComponentType, ReactWrapper } from 'enzyme';
+import type { Datatable } from 'src/plugins/expressions/public';
+import { getLegendAction } from './get_legend_action';
+import { LegendActionPopover } from '../shared_components';
+
+const table: Datatable = {
+ type: 'datatable',
+ columns: [
+ { id: 'a', name: 'A', meta: { type: 'string' } },
+ { id: 'b', name: 'B', meta: { type: 'number' } },
+ ],
+ rows: [
+ { a: 'Hi', b: 2 },
+ { a: 'Test', b: 4 },
+ { a: 'Foo', b: 6 },
+ ],
+};
+
+describe('getLegendAction', function () {
+ let wrapperProps: LegendActionProps;
+ const Component: ComponentType = getLegendAction(table, jest.fn());
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapperProps = {
+ color: 'rgb(109, 204, 177)',
+ label: 'Bar',
+ series: ([
+ {
+ specId: 'donut',
+ key: 'Bar',
+ },
+ ] as unknown) as SeriesIdentifier[],
+ };
+ });
+
+ it('is not rendered if row does not exist', () => {
+ wrapper = mountWithIntl( );
+ expect(wrapper).toEqual({});
+ expect(wrapper.find(EuiPopover).length).toBe(0);
+ });
+
+ it('is rendered if row is detected', () => {
+ const newProps = {
+ ...wrapperProps,
+ label: 'Hi',
+ series: ([
+ {
+ specId: 'donut',
+ key: 'Hi',
+ },
+ ] as unknown) as SeriesIdentifier[],
+ };
+ wrapper = mountWithIntl( );
+ expect(wrapper.find(EuiPopover).length).toBe(1);
+ expect(wrapper.find(EuiPopover).prop('title')).toEqual('Hi, filter options');
+ expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({
+ data: [
+ {
+ column: 0,
+ row: 0,
+ table,
+ value: 'Hi',
+ },
+ ],
+ });
+ });
+});
diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx
new file mode 100644
index 0000000000000..9f16ad863a415
--- /dev/null
+++ b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import type { LegendAction } from '@elastic/charts';
+import type { Datatable } from 'src/plugins/expressions/public';
+import type { LensFilterEvent } from '../types';
+import { LegendActionPopover } from '../shared_components';
+
+export const getLegendAction = (
+ table: Datatable,
+ onFilter: (data: LensFilterEvent['data']) => void
+): LegendAction =>
+ React.memo(({ series: [pieSeries], label }) => {
+ const data = table.columns.reduce((acc, { id }, column) => {
+ const value = pieSeries.key;
+ const row = table.rows.findIndex((r) => r[id] === value);
+ if (row > -1) {
+ acc.push({
+ table,
+ column,
+ row,
+ value,
+ });
+ }
+
+ return acc;
+ }, []);
+
+ if (data.length === 0) {
+ return null;
+ }
+
+ const context: LensFilterEvent['data'] = {
+ data,
+ };
+
+ return ;
+ });
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
index 6c1cbe63a5a3e..f329cfe1bb8b9 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
@@ -38,6 +38,7 @@ import {
SeriesLayer,
} from '../../../../../src/plugins/charts/public';
import { LensIconChartDonut } from '../assets/chart_donut';
+import { getLegendAction } from './get_legend_action';
declare global {
interface Window {
@@ -281,6 +282,7 @@ export function PieComponent(
onElementClick={
props.renderMode !== 'noInteractivity' ? onElementClickHandler : undefined
}
+ legendAction={getLegendAction(firstTable, onClickValue)}
theme={{
...chartTheme,
background: {
diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts
index cf8536884acdf..c200a18a25caf 100644
--- a/x-pack/plugins/lens/public/shared_components/index.ts
+++ b/x-pack/plugins/lens/public/shared_components/index.ts
@@ -13,3 +13,4 @@ export { TooltipWrapper } from './tooltip_wrapper';
export * from './coloring';
export { useDebouncedValue } from './debounced_value';
export * from './helpers';
+export { LegendActionPopover } from './legend_action_popover';
diff --git a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx
new file mode 100644
index 0000000000000..e344cb5289f51
--- /dev/null
+++ b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx
@@ -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 React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui';
+import type { LensFilterEvent } from '../types';
+import { desanitizeFilterContext } from '../utils';
+
+export interface LegendActionPopoverProps {
+ /**
+ * Determines the panels label
+ */
+ label: string;
+ /**
+ * Callback on filter value
+ */
+ onFilter: (data: LensFilterEvent['data']) => void;
+ /**
+ * Determines the filter event data
+ */
+ context: LensFilterEvent['data'];
+}
+
+export const LegendActionPopover: React.FunctionComponent = ({
+ label,
+ onFilter,
+ context,
+}) => {
+ const [popoverOpen, setPopoverOpen] = useState(false);
+ const panels: EuiContextMenuPanelDescriptor[] = [
+ {
+ id: 'main',
+ title: label,
+ items: [
+ {
+ name: i18n.translate('xpack.lens.shared.legend.filterForValueButtonAriaLabel', {
+ defaultMessage: 'Filter for value',
+ }),
+ 'data-test-subj': `legend-${label}-filterIn`,
+ icon: ,
+ onClick: () => {
+ setPopoverOpen(false);
+ onFilter(desanitizeFilterContext(context));
+ },
+ },
+ {
+ name: i18n.translate('xpack.lens.shared.legend.filterOutValueButtonAriaLabel', {
+ defaultMessage: 'Filter out value',
+ }),
+ 'data-test-subj': `legend-${label}-filterOut`,
+ icon: ,
+ onClick: () => {
+ setPopoverOpen(false);
+ onFilter(desanitizeFilterContext({ ...context, negate: true }));
+ },
+ },
+ ],
+ },
+ ];
+
+ const Button = (
+ setPopoverOpen(!popoverOpen)}
+ onClick={() => setPopoverOpen(!popoverOpen)}
+ >
+
+
+ );
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
index f9b4e33072c81..1f647680408d7 100644
--- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
+++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
@@ -7,6 +7,13 @@ exports[`xy_expression XYChart component it renders area 1`] = `
{
});
});
+ test('onElementClick returns correct context data for date histogram', () => {
+ const geometry: GeometryValue = {
+ x: 1585758120000,
+ y: 1,
+ accessor: 'y1',
+ mark: null,
+ datum: {},
+ };
+ const series = {
+ key: 'spec{d}yAccessor{d}splitAccessors{b-2}',
+ specId: 'd',
+ yAccessor: 'yAccessorId',
+ splitAccessors: {},
+ seriesKeys: ['yAccessorId'],
+ };
+
+ const { args } = sampleArgs();
+
+ const wrapper = mountWithIntl(
+
+ );
+
+ wrapper.find(Settings).first().prop('onElementClick')!([
+ [geometry, series as XYChartSeriesIdentifier],
+ ]);
+
+ expect(onClickValue).toHaveBeenCalledWith({
+ data: [
+ {
+ column: 0,
+ row: 0,
+ table: dateHistogramData.tables.timeLayer,
+ value: 1585758120000,
+ },
+ ],
+ timeFieldName: 'order_date',
+ });
+ });
+
+ test('onElementClick returns correct context data for numeric histogram', () => {
+ const { args } = sampleArgs();
+
+ const numberLayer: LayerArgs = {
+ layerId: 'numberLayer',
+ hide: false,
+ xAccessor: 'xAccessorId',
+ yScaleType: 'linear',
+ xScaleType: 'linear',
+ isHistogram: true,
+ seriesType: 'bar_stacked',
+ accessors: ['yAccessorId'],
+ palette: mockPaletteOutput,
+ };
+
+ const numberHistogramData: LensMultiTable = {
+ type: 'lens_multitable',
+ tables: {
+ numberLayer: {
+ type: 'datatable',
+ rows: [
+ {
+ xAccessorId: 5,
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 7,
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 8,
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 10,
+ yAccessorId: 1,
+ },
+ ],
+ columns: [
+ {
+ id: 'xAccessorId',
+ name: 'bytes',
+ meta: { type: 'number' },
+ },
+ {
+ id: 'yAccessorId',
+ name: 'Count of records',
+ meta: { type: 'number' },
+ },
+ ],
+ },
+ },
+ dateRange: {
+ fromDate: new Date('2020-04-01T16:14:16.246Z'),
+ toDate: new Date('2020-04-01T17:15:41.263Z'),
+ },
+ };
+ const geometry: GeometryValue = {
+ x: 5,
+ y: 1,
+ accessor: 'y1',
+ mark: null,
+ datum: {},
+ };
+ const series = {
+ key: 'spec{d}yAccessor{d}splitAccessors{b-2}',
+ specId: 'd',
+ yAccessor: 'yAccessorId',
+ splitAccessors: {},
+ seriesKeys: ['yAccessorId'],
+ };
+
+ const wrapper = mountWithIntl(
+
+ );
+
+ wrapper.find(Settings).first().prop('onElementClick')!([
+ [geometry, series as XYChartSeriesIdentifier],
+ ]);
+
+ expect(onClickValue).toHaveBeenCalledWith({
+ data: [
+ {
+ column: 0,
+ row: 0,
+ table: numberHistogramData.tables.numberLayer,
+ value: 5,
+ },
+ ],
+ timeFieldName: undefined,
+ });
+ });
+
test('returns correct original data for ordinal x axis with special formatter', () => {
const geometry: GeometryValue = { x: 'BAR', y: 1, accessor: 'y1', mark: null, datum: {} };
const series = {
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
index 4cd2b55e8d424..1de5cf6b30533 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
@@ -58,6 +58,7 @@ import { fittingFunctionDefinitions, getFitOptions } from './fitting_functions';
import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axes_configuration';
import { getColorAssignments } from './color_assignment';
import { getXDomain, XyEndzones } from './x_domain';
+import { getLegendAction } from './get_legend_action';
declare global {
interface Window {
@@ -390,6 +391,7 @@ export function XYChart({
);
const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.meta?.params);
const layersAlreadyFormatted: Record = {};
+
// This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers
const safeXAccessorLabelRenderer = (value: unknown): string =>
xAxisColumn && layersAlreadyFormatted[xAxisColumn.id]
@@ -560,9 +562,9 @@ export function XYChart({
value: pointValue,
});
}
-
- const xAxisFieldName = table.columns.find((el) => el.id === layer.xAccessor)?.meta?.field;
- const timeFieldName = xDomain && xAxisFieldName;
+ const currentColumnMeta = table.columns.find((el) => el.id === layer.xAccessor)?.meta;
+ const xAxisFieldName = currentColumnMeta?.field;
+ const isDateField = currentColumnMeta?.type === 'date';
const context: LensFilterEvent['data'] = {
data: points.map((point) => ({
@@ -571,7 +573,7 @@ export function XYChart({
value: point.value,
table,
})),
- timeFieldName,
+ timeFieldName: xDomain && isDateField ? xAxisFieldName : undefined,
};
onClickValue(desanitizeFilterContext(context));
};
@@ -629,6 +631,13 @@ export function XYChart({
xDomain={xDomain}
onBrushEnd={renderMode !== 'noInteractivity' ? brushHandler : undefined}
onElementClick={renderMode !== 'noInteractivity' ? clickHandler : undefined}
+ legendAction={getLegendAction(
+ filteredLayers,
+ data.tables,
+ onClickValue,
+ formatFactory,
+ layersAlreadyFormatted
+ )}
showLegendExtra={isHistogramViz && valuesInLegend}
/>
diff --git a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx
new file mode 100644
index 0000000000000..e4edfe918a242
--- /dev/null
+++ b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx
@@ -0,0 +1,232 @@
+/*
+ * Copyright 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 { LegendActionProps, SeriesIdentifier } from '@elastic/charts';
+import { EuiPopover } from '@elastic/eui';
+import { mountWithIntl } from '@kbn/test/jest';
+import { ComponentType, ReactWrapper } from 'enzyme';
+import type { LayerArgs } from './types';
+import type { LensMultiTable } from '../types';
+import { getLegendAction } from './get_legend_action';
+import { LegendActionPopover } from '../shared_components';
+
+const sampleLayer = {
+ layerId: 'first',
+ seriesType: 'line',
+ xAccessor: 'c',
+ accessors: ['a', 'b'],
+ splitAccessor: 'splitAccessorId',
+ columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
+ xScaleType: 'ordinal',
+ yScaleType: 'linear',
+ isHistogram: false,
+} as LayerArgs;
+
+const tables = {
+ first: {
+ type: 'datatable',
+ rows: [
+ {
+ xAccessorId: 1585758120000,
+ splitAccessorId: "Men's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585758360000,
+ splitAccessorId: "Women's Accessories",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585758360000,
+ splitAccessorId: "Women's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585759380000,
+ splitAccessorId: "Men's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585759380000,
+ splitAccessorId: "Men's Shoes",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585759380000,
+ splitAccessorId: "Women's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585760700000,
+ splitAccessorId: "Men's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585760760000,
+ splitAccessorId: "Men's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585760760000,
+ splitAccessorId: "Men's Shoes",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585761120000,
+ splitAccessorId: "Men's Shoes",
+ yAccessorId: 1,
+ },
+ ],
+ columns: [
+ {
+ id: 'xAccessorId',
+ name: 'order_date per minute',
+ meta: {
+ type: 'date',
+ field: 'order_date',
+ source: 'esaggs',
+ index: 'indexPatternId',
+ sourceParams: {
+ indexPatternId: 'indexPatternId',
+ type: 'date_histogram',
+ params: {
+ field: 'order_date',
+ timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' },
+ useNormalizedEsInterval: true,
+ scaleMetricValues: false,
+ interval: '1m',
+ drop_partials: false,
+ min_doc_count: 0,
+ extended_bounds: {},
+ },
+ },
+ params: { id: 'date', params: { pattern: 'HH:mm' } },
+ },
+ },
+ {
+ id: 'splitAccessorId',
+ name: 'Top values of category.keyword',
+ meta: {
+ type: 'string',
+ field: 'category.keyword',
+ source: 'esaggs',
+ index: 'indexPatternId',
+ sourceParams: {
+ indexPatternId: 'indexPatternId',
+ type: 'terms',
+ params: {
+ field: 'category.keyword',
+ orderBy: 'yAccessorId',
+ order: 'desc',
+ size: 3,
+ otherBucket: false,
+ otherBucketLabel: 'Other',
+ missingBucket: false,
+ missingBucketLabel: 'Missing',
+ },
+ },
+ params: {
+ id: 'terms',
+ params: {
+ id: 'string',
+ otherBucketLabel: 'Other',
+ missingBucketLabel: 'Missing',
+ parsedUrl: {
+ origin: 'http://localhost:5601',
+ pathname: '/jiy/app/kibana',
+ basePath: '/jiy',
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'yAccessorId',
+ name: 'Count of records',
+ meta: {
+ type: 'number',
+ source: 'esaggs',
+ index: 'indexPatternId',
+ sourceParams: {
+ indexPatternId: 'indexPatternId',
+ params: {},
+ },
+ params: { id: 'number' },
+ },
+ },
+ ],
+ },
+} as LensMultiTable['tables'];
+
+describe('getLegendAction', function () {
+ let wrapperProps: LegendActionProps;
+ const Component: ComponentType = getLegendAction(
+ [sampleLayer],
+ tables,
+ jest.fn(),
+ jest.fn(),
+ {}
+ );
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapperProps = {
+ color: 'rgb(109, 204, 177)',
+ label: "Women's Accessories",
+ series: ([
+ {
+ seriesKeys: ["Women's Accessories", 'test'],
+ },
+ ] as unknown) as SeriesIdentifier[],
+ };
+ });
+
+ it('is not rendered if not layer is detected', () => {
+ wrapper = mountWithIntl( );
+ expect(wrapper).toEqual({});
+ expect(wrapper.find(EuiPopover).length).toBe(0);
+ });
+
+ it('is rendered if row does not exist', () => {
+ const newProps = {
+ ...wrapperProps,
+ series: ([
+ {
+ seriesKeys: ['test', 'b'],
+ },
+ ] as unknown) as SeriesIdentifier[],
+ };
+ wrapper = mountWithIntl( );
+ expect(wrapper).toEqual({});
+ expect(wrapper.find(EuiPopover).length).toBe(0);
+ });
+
+ it('is rendered if layer is detected', () => {
+ const newProps = {
+ ...wrapperProps,
+ series: ([
+ {
+ seriesKeys: ["Women's Accessories", 'b'],
+ },
+ ] as unknown) as SeriesIdentifier[],
+ };
+ wrapper = mountWithIntl( );
+ expect(wrapper.find(EuiPopover).length).toBe(1);
+ expect(wrapper.find(EuiPopover).prop('title')).toEqual("Women's Accessories, filter options");
+ expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({
+ data: [
+ {
+ column: 1,
+ row: 1,
+ table: tables.first,
+ value: "Women's Accessories",
+ },
+ ],
+ });
+ });
+});
diff --git a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx
new file mode 100644
index 0000000000000..c99bf948d6e37
--- /dev/null
+++ b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts';
+import type { LayerArgs } from './types';
+import type { LensMultiTable, LensFilterEvent, FormatFactory } from '../types';
+import { LegendActionPopover } from '../shared_components';
+
+export const getLegendAction = (
+ filteredLayers: LayerArgs[],
+ tables: LensMultiTable['tables'],
+ onFilter: (data: LensFilterEvent['data']) => void,
+ formatFactory: FormatFactory,
+ layersAlreadyFormatted: Record
+): LegendAction =>
+ React.memo(({ series: [xySeries] }) => {
+ const series = xySeries as XYChartSeriesIdentifier;
+ const layer = filteredLayers.find((l) =>
+ series.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString()))
+ );
+
+ if (!layer || !layer.splitAccessor) {
+ return null;
+ }
+
+ const splitLabel = series.seriesKeys[0] as string;
+ const accessor = layer.splitAccessor;
+
+ const table = tables[layer.layerId];
+ const splitColumn = table.columns.find(({ id }) => id === layer.splitAccessor);
+ const formatter = formatFactory(splitColumn && splitColumn.meta?.params);
+
+ const rowIndex = table.rows.findIndex((row) => {
+ if (layersAlreadyFormatted[accessor]) {
+ // stringify the value to compare with the chart value
+ return formatter.convert(row[accessor]) === splitLabel;
+ }
+ return row[accessor] === splitLabel;
+ });
+
+ if (rowIndex < 0) return null;
+
+ const data = [
+ {
+ row: rowIndex,
+ column: table.columns.findIndex((col) => col.id === accessor),
+ value: accessor ? table.rows[rowIndex][accessor] : splitLabel,
+ table,
+ },
+ ];
+
+ const context: LensFilterEvent['data'] = {
+ data,
+ };
+
+ return (
+
+ );
+ });
diff --git a/x-pack/plugins/lens/server/usage/visualization_counts.ts b/x-pack/plugins/lens/server/usage/visualization_counts.ts
index f0c48fb1152e8..6e79d5f342377 100644
--- a/x-pack/plugins/lens/server/usage/visualization_counts.ts
+++ b/x-pack/plugins/lens/server/usage/visualization_counts.ts
@@ -75,7 +75,7 @@ export async function getVisualizationCounts(
},
});
- // @ts-expect-error @elastic/elasticsearch no way to declare aggregations for search response
+ // @ts-expect-error specify aggregations type explicitly
const buckets = results.aggregations.groups.buckets;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap
index 95921fa61233c..90a3eb98c64a1 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`AddLicense component when license is active should display correct verbiage 1`] = `"Update your license If you already have a new license, upload it now.
"`;
+exports[`AddLicense component when license is active should display correct verbiage 1`] = `"Update your license If you already have a new license, upload it now.
"`;
-exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"Update your license If you already have a new license, upload it now.
"`;
+exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"Update your license If you already have a new license, upload it now.
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap
new file mode 100644
index 0000000000000..047e311f3d325
--- /dev/null
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap
@@ -0,0 +1,5 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LicenseStatus component should display display warning is expired 1`] = `"
"`;
+
+exports[`LicenseStatus component should display normally when license is active 1`] = `"
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap
deleted file mode 100644
index 9bd1c878f8679..0000000000000
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap
+++ /dev/null
@@ -1,5 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`LicenseStatus component should display display warning is expired 1`] = `"
Your Platinum license has expired
Your license expired on
"`;
-
-exports[`LicenseStatus component should display normally when license is active 1`] = `"
Your Gold license is active
Your license will expire on October 12, 2099 7:00 PM EST
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
index 4d8b653c4b10d..fda479f2888ce 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `""`;
+exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `""`;
-exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `""`;
+exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `""`;
-exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `""`;
+exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `""`;
-exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `""`;
+exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `""`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
index be634a5b4f748..4fa45c4bec5ce 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RevertToBasic component should display when license is about to expire 1`] = `""`;
+exports[`RevertToBasic component should display when license is about to expire 1`] = `""`;
-exports[`RevertToBasic component should display when license is expired 1`] = `""`;
+exports[`RevertToBasic component should display when license is expired 1`] = `""`;
-exports[`RevertToBasic component should display when trial is active 1`] = `""`;
+exports[`RevertToBasic component should display when trial is active 1`] = `""`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
index 1cacadb824630..622bff86ead16 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`StartTrial component when trial is allowed display for basic license 1`] = `""`;
+exports[`StartTrial component when trial is allowed display for basic license 1`] = `""`;
-exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `""`;
+exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `""`;
-exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `""`;
+exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `""`;
-exports[`StartTrial component when trial is allowed should display for gold license 1`] = `""`;
+exports[`StartTrial component when trial is allowed should display for gold license 1`] = `""`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
index 9f89179d207e0..45e7055f4db2b 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
@@ -262,16 +262,18 @@ exports[`UploadLicense should display a modal when license requires acknowledgem
uploadLicenseStatus={[Function]}
>
@@ -1301,16 +1303,18 @@ exports[`UploadLicense should display an error when ES says license is expired 1
uploadLicenseStatus={[Function]}
>
@@ -1413,19 +1417,29 @@ exports[`UploadLicense should display an error when ES says license is expired 1
-
-
+
- The supplied license has expired.
-
-
+
+
+ The supplied license has expired.
+
+
+
+
@@ -2031,16 +2045,18 @@ exports[`UploadLicense should display an error when ES says license is invalid 1
uploadLicenseStatus={[Function]}
>
@@ -2143,19 +2159,29 @@ exports[`UploadLicense should display an error when ES says license is invalid 1
-
-
+
- The supplied license is not valid for this product.
-
-
+
+
+ The supplied license is not valid for this product.
+
+
+
+
@@ -2761,16 +2787,18 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`]
uploadLicenseStatus={[Function]}
>
@@ -2873,19 +2901,29 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`]
-
-
+
- Error encountered uploading license: Check your license file.
-
-
+
+
+ Error encountered uploading license: Check your license file.
+
+
+
+
@@ -3491,16 +3529,18 @@ exports[`UploadLicense should display error when ES returns error 1`] = `
uploadLicenseStatus={[Function]}
>
@@ -3603,19 +3643,29 @@ exports[`UploadLicense should display error when ES returns error 1`] = `
-
-
+
- Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled
-
-
+
+
+ Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled
+
+
+
+
diff --git a/x-pack/plugins/license_management/__jest__/license_status.test.js b/x-pack/plugins/license_management/__jest__/license_page_header.test.js
similarity index 83%
rename from x-pack/plugins/license_management/__jest__/license_status.test.js
rename to x-pack/plugins/license_management/__jest__/license_page_header.test.js
index 898667e13a1b3..56a71eb8d252e 100644
--- a/x-pack/plugins/license_management/__jest__/license_status.test.js
+++ b/x-pack/plugins/license_management/__jest__/license_page_header.test.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { LicenseStatus } from '../public/application/sections/license_dashboard/license_status';
+import { LicensePageHeader } from '../public/application/sections/license_dashboard/license_page_header';
import { createMockLicense, getComponent } from './util';
describe('LicenseStatus component', () => {
@@ -14,7 +14,7 @@ describe('LicenseStatus component', () => {
{
license: createMockLicense('gold'),
},
- LicenseStatus
+ LicensePageHeader
);
expect(rendered.html()).toMatchSnapshot();
});
@@ -23,7 +23,7 @@ describe('LicenseStatus component', () => {
{
license: createMockLicense('platinum', 0),
},
- LicenseStatus
+ LicensePageHeader
);
expect(rendered.html()).toMatchSnapshot();
});
diff --git a/x-pack/plugins/license_management/kibana.json b/x-pack/plugins/license_management/kibana.json
index 1f925a453898e..be2e21c7eb41e 100644
--- a/x-pack/plugins/license_management/kibana.json
+++ b/x-pack/plugins/license_management/kibana.json
@@ -9,6 +9,7 @@
"extraPublicDirs": ["common/constants"],
"requiredBundles": [
"telemetryManagementSection",
+ "esUiShared",
"kibanaReact"
]
}
diff --git a/x-pack/plugins/license_management/public/application/app.js b/x-pack/plugins/license_management/public/application/app.js
index 3bfa22dd72921..4b5a6144dbdc9 100644
--- a/x-pack/plugins/license_management/public/application/app.js
+++ b/x-pack/plugins/license_management/public/application/app.js
@@ -10,7 +10,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { LicenseDashboard, UploadLicense } from './sections';
import { Switch, Route } from 'react-router-dom';
import { APP_PERMISSION } from '../../common/constants';
-import { EuiPageBody, EuiEmptyPrompt, EuiText, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui';
+import { SectionLoading } from '../shared_imports';
+import { EuiPageContent, EuiPageBody, EuiEmptyPrompt } from '@elastic/eui';
export class App extends Component {
componentDidMount() {
@@ -23,52 +24,50 @@ export class App extends Component {
if (permissionsLoading) {
return (
-
}
- body={
-
-
-
- }
- data-test-subj="sectionLoading"
- />
+
+
+
+
+
);
}
if (permissionsError) {
+ const error = permissionsError?.data?.message;
+
return (
-
- }
- color="danger"
- iconType="alert"
- >
- {permissionsError.data && permissionsError.data.message ? (
-
{permissionsError.data.message}
- ) : null}
-
+
+
+
+
+ }
+ body={error ? {error}
: null}
+ />
+
);
}
if (!hasPermission) {
return (
-
+
+
-
+
}
body={
@@ -82,7 +81,7 @@ export class App extends Component {
}
/>
-
+
);
}
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js
index 4120b2280a7a6..90de14b167e52 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js
@@ -18,6 +18,7 @@ export const AddLicense = ({ uploadPath = `/upload_license` }) => {
return (
{} }) => {
useEffect(() => {
@@ -19,17 +20,19 @@ export const LicenseDashboard = ({ setBreadcrumb, telemetry } = { setBreadcrumb:
});
return (
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js
similarity index 80%
rename from x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js
rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js
index efd4da2770db4..303e30040ab50 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { LicenseStatus } from './license_status.container';
+export { LicensePageHeader } from './license_page_header';
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js
new file mode 100644
index 0000000000000..df41d46ac5789
--- /dev/null
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js
@@ -0,0 +1,106 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiPageHeader, EuiSpacer } from '@elastic/eui';
+
+import { getLicenseState } from '../../../store/reducers/license_management';
+
+export const ActiveLicensePageHeader = ({ license, ...props }) => {
+ return (
+
+
+
+ }
+ description={
+
+ {license.expirationDate ? (
+ {license.expirationDate},
+ }}
+ />
+ ) : (
+
+ )}
+
+ }
+ />
+ );
+};
+
+export const ExpiredLicensePageHeader = ({ license, ...props }) => {
+ return (
+
+
+
+ }
+ description={
+
+ {license.expirationDate},
+ }}
+ />
+
+ }
+ />
+ );
+};
+
+export const LicensePageHeader = () => {
+ const license = useSelector(getLicenseState);
+
+ return (
+ <>
+ {license.isExpired ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js
deleted file mode 100644
index 01577e79fd6ec..0000000000000
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { LicenseStatus as PresentationComponent } from './license_status';
-import { connect } from 'react-redux';
-import {
- getLicense,
- getExpirationDateFormatted,
- isExpired,
-} from '../../../store/reducers/license_management';
-import { i18n } from '@kbn/i18n';
-
-const mapStateToProps = (state) => {
- const { isActive, type } = getLicense(state);
- return {
- status: isActive
- ? i18n.translate('xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText', {
- defaultMessage: 'Active',
- })
- : i18n.translate(
- 'xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText',
- {
- defaultMessage: 'Inactive',
- }
- ),
- type,
- isExpired: isExpired(state),
- expiryDate: getExpirationDateFormatted(state),
- };
-};
-
-export const LicenseStatus = connect(mapStateToProps)(PresentationComponent);
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js
deleted file mode 100644
index 5f7e59bf1ceba..0000000000000
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { Fragment } from 'react';
-
-import {
- EuiIcon,
- EuiFlexGroup,
- EuiFlexItem,
- EuiText,
- EuiTitle,
- EuiSpacer,
- EuiTextAlign,
-} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-export class LicenseStatus extends React.PureComponent {
- render() {
- const { isExpired, status, type, expiryDate } = this.props;
- const typeTitleCase = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase();
- let icon;
- let title;
- let message;
- if (isExpired) {
- icon = ;
- message = (
-
- {expiryDate},
- }}
- />
-
- );
- title = (
-
- );
- } else {
- icon = ;
- message = expiryDate ? (
-
- {expiryDate},
- }}
- />
-
- ) : (
-
-
-
- );
- title = (
-
- );
- }
- return (
-
-
- {icon}
-
-
- {title}
-
-
-
-
-
-
- {message}
-
- );
- }
-}
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
index 8c694cf27765a..e578c372b9c9f 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
@@ -37,6 +37,7 @@ export const RequestTrialExtension = ({ shouldShowRequestTrialExtension }) => {
return (
{this.acknowledgeModal()}
{
{this.acknowledgeModal(dependencies!.docLinks)}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
- {this.acknowledgeModal()}
+ {this.acknowledgeModal()}
-
-
-
-
-
- {currentLicenseType.toUpperCase()},
- }}
- />
-
-
-
-
-
-
-
-
- }
- onChange={this.handleFile}
+
+
+
+
+
+ {currentLicenseType.toUpperCase()},
+ }}
+ />
+
+
+
+
+
+
+
+
+ }
+ onChange={this.handleFile}
+ />
+
+
+
+
+ {shouldShowTelemetryOptIn(telemetry) && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {applying ? (
+
-
-
-
-
- {shouldShowTelemetryOptIn(telemetry) && (
-
- )}
-
-
-
-
+ ) : (
-
-
-
-
- {applying ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
-
+ )}
+
+
+
+
+
+
);
}
}
diff --git a/x-pack/plugins/license_management/public/application/store/reducers/license_management.js b/x-pack/plugins/license_management/public/application/store/reducers/license_management.js
index 20e31cf89da72..1a985cd8ee623 100644
--- a/x-pack/plugins/license_management/public/application/store/reducers/license_management.js
+++ b/x-pack/plugins/license_management/public/application/store/reducers/license_management.js
@@ -6,6 +6,10 @@
*/
import { combineReducers } from 'redux';
+import { i18n } from '@kbn/i18n';
+import { capitalize } from 'lodash';
+import { createSelector } from 'reselect';
+
import { license } from './license';
import { uploadStatus } from './upload_status';
import { startBasicStatus } from './start_basic_license_status';
@@ -135,3 +139,31 @@ export const startBasicLicenseNeedsAcknowledgement = (state) => {
export const getStartBasicMessages = (state) => {
return state.startBasicStatus.messages;
};
+
+export const getLicenseState = createSelector(
+ getLicense,
+ getExpirationDateFormatted,
+ isExpired,
+ (license, expirationDate, isExpired) => {
+ const { isActive, type } = license;
+
+ return {
+ type: capitalize(type),
+ isExpired,
+ expirationDate,
+ status: isActive
+ ? i18n.translate(
+ 'xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText',
+ {
+ defaultMessage: 'active',
+ }
+ )
+ : i18n.translate(
+ 'xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText',
+ {
+ defaultMessage: 'inactive',
+ }
+ ),
+ };
+ }
+);
diff --git a/x-pack/plugins/license_management/public/shared_imports.ts b/x-pack/plugins/license_management/public/shared_imports.ts
new file mode 100644
index 0000000000000..695432684a660
--- /dev/null
+++ b/x-pack/plugins/license_management/public/shared_imports.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 { SectionLoading } from '../../../../src/plugins/es_ui_shared/public/';
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts
index ec46038c397e5..212db40f3168c 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts
@@ -1697,9 +1697,9 @@ describe('Exception builder helpers', () => {
namespaceType: 'single',
ruleName: 'rule name',
});
- const exceptions = filterExceptionItems([{ ...rest, meta }]);
+ const exceptions = filterExceptionItems([{ ...rest, entries: [getEntryMatchMock()], meta }]);
- expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]);
+ expect(exceptions).toEqual([{ ...rest, entries: [getEntryMatchMock()], meta: undefined }]);
});
});
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts
index bdcb4224eed9c..4987de321c556 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts
@@ -48,6 +48,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
@@ -83,6 +84,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
@@ -122,6 +124,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: true,
})
);
@@ -132,7 +135,7 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)',
+ '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -157,6 +160,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
@@ -167,7 +171,79 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)',
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
+ http: mockKibanaHttpService,
+ namespaceTypes: 'single,agnostic',
+ pagination: { page: 1, perPage: 20 },
+ signal: new AbortController().signal,
+ });
+ });
+ });
+
+ test('fetches event filters lists if "showEventFilters" is true', async () => {
+ const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
+
+ await act(async () => {
+ const { waitForNextUpdate } = renderHook(() =>
+ useExceptionLists({
+ errorMessage: 'Uh oh',
+ filterOptions: {},
+ http: mockKibanaHttpService,
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
+ pagination: {
+ page: 1,
+ perPage: 20,
+ total: 0,
+ },
+ showEventFilters: true,
+ showTrustedApps: false,
+ })
+ );
+ // NOTE: First `waitForNextUpdate` is initialization
+ // Second call applies the params
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
+ filters:
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
+ http: mockKibanaHttpService,
+ namespaceTypes: 'single,agnostic',
+ pagination: { page: 1, perPage: 20 },
+ signal: new AbortController().signal,
+ });
+ });
+ });
+
+ test('does not fetch event filters lists if "showEventFilters" is false', async () => {
+ const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
+
+ await act(async () => {
+ const { waitForNextUpdate } = renderHook(() =>
+ useExceptionLists({
+ errorMessage: 'Uh oh',
+ filterOptions: {},
+ http: mockKibanaHttpService,
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
+ pagination: {
+ page: 1,
+ perPage: 20,
+ total: 0,
+ },
+ showEventFilters: false,
+ showTrustedApps: false,
+ })
+ );
+ // NOTE: First `waitForNextUpdate` is initialization
+ // Second call applies the params
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
+ filters:
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -195,6 +271,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
@@ -205,7 +282,7 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)',
+ '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -228,6 +305,7 @@ describe('useExceptionLists', () => {
namespaceTypes,
notifications,
pagination,
+ showEventFilters,
showTrustedApps,
}) =>
useExceptionLists({
@@ -237,6 +315,7 @@ describe('useExceptionLists', () => {
namespaceTypes,
notifications,
pagination,
+ showEventFilters,
showTrustedApps,
}),
{
@@ -251,6 +330,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
},
}
@@ -271,6 +351,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
});
// NOTE: Only need one call here because hook already initilaized
@@ -298,6 +379,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
@@ -336,6 +418,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
diff --git a/x-pack/plugins/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 94a049d10cc45..0000000000000
--- 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 4ccff2dd000b9..77e82bf0f7578 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 37a8e8063c4ed..fa065e701184e 100644
--- a/x-pack/plugins/maps/common/constants.ts
+++ b/x-pack/plugins/maps/common/constants.ts
@@ -58,15 +58,14 @@ export const KBN_IS_CENTROID_FEATURE = '__kbn_is_centroid_feature__';
export const MVT_TOKEN_PARAM_NAME = 'token';
-const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`;
export function getNewMapPath() {
- return MAP_BASE_URL;
+ return `/${MAPS_APP_PATH}/${MAP_PATH}`;
}
-export function getExistingMapPath(id: string) {
- return `${MAP_BASE_URL}/${id}`;
+export function getFullPath(id: string | undefined) {
+ return `/${MAPS_APP_PATH}${getEditPath(id)}`;
}
-export function getEditPath(id: string) {
- return `/${MAP_PATH}/${id}`;
+export function getEditPath(id: string | undefined) {
+ return id ? `/${MAP_PATH}/${id}` : `/${MAP_PATH}`;
}
export enum LAYER_TYPE {
diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
index 07de57d0ac832..d1690ddfff43d 100644
--- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
@@ -66,6 +66,7 @@ export type VectorSourceRequestMeta = MapFilters & {
applyGlobalTime: boolean;
fieldNames: string[];
geogridPrecision?: number;
+ timesiceMaskField?: string;
sourceQuery?: MapQuery;
sourceMeta: VectorSourceSyncMeta;
};
@@ -84,6 +85,9 @@ export type VectorStyleRequestMeta = MapFilters & {
export type ESSearchSourceResponseMeta = {
areResultsTrimmed?: boolean;
resultsCount?: number;
+ // results time extent, either Kibana time range or timeslider time slice
+ timeExtent?: Timeslice;
+ isTimeExtentForTimeslice?: boolean;
// top hits meta
areEntitiesTrimmed?: boolean;
diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
index 6dd454137be7d..9bfa74825c338 100644
--- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
+++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
@@ -22,7 +22,6 @@ import {
LAYER_STYLE_TYPE,
FIELD_ORIGIN,
} from '../../../../common/constants';
-import { isTotalHitsGreaterThan, TotalHits } from '../../../../common/elasticsearch_util';
import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source';
import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
import { IESSource } from '../../sources/es_source';
@@ -35,6 +34,7 @@ import {
DynamicStylePropertyOptions,
StylePropertyOptions,
LayerDescriptor,
+ Timeslice,
VectorLayerDescriptor,
VectorSourceRequestMeta,
VectorStylePropertiesDescriptor,
@@ -46,10 +46,6 @@ import { isSearchSourceAbortError } from '../../sources/es_source/es_source';
const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID';
-interface CountData {
- isSyncClustered: boolean;
-}
-
function getAggType(
dynamicProperty: IDynamicStyleProperty
): AGG_TYPE.AVG | AGG_TYPE.TERMS {
@@ -216,7 +212,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
let isClustered = false;
const countDataRequest = this.getDataRequest(ACTIVE_COUNT_DATA_ID);
if (countDataRequest) {
- const requestData = countDataRequest.getData() as CountData;
+ const requestData = countDataRequest.getData() as { isSyncClustered: boolean };
if (requestData && requestData.isSyncClustered) {
isClustered = true;
}
@@ -294,7 +290,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
async syncData(syncContext: DataRequestContext) {
const dataRequestId = ACTIVE_COUNT_DATA_ID;
const requestToken = Symbol(`layer-active-count:${this.getId()}`);
- const searchFilters: VectorSourceRequestMeta = this._getSearchFilters(
+ const searchFilters: VectorSourceRequestMeta = await this._getSearchFilters(
syncContext.dataFilters,
this.getSource(),
this.getCurrentStyle()
@@ -305,6 +301,9 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
prevDataRequest: this.getDataRequest(dataRequestId),
nextMeta: searchFilters,
extentAware: source.isFilterByMapBounds(),
+ getUpdateDueToTimeslice: (timeslice?: Timeslice) => {
+ return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice);
+ },
});
let activeSource;
@@ -322,22 +321,11 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
let isSyncClustered;
try {
syncContext.startLoading(dataRequestId, requestToken, searchFilters);
- const abortController = new AbortController();
- syncContext.registerCancelCallback(requestToken, () => abortController.abort());
- const maxResultWindow = await this._documentSource.getMaxResultWindow();
- const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0);
- searchSource.setField('trackTotalHits', maxResultWindow + 1);
- const resp = await searchSource.fetch({
- abortSignal: abortController.signal,
- sessionId: syncContext.dataFilters.searchSessionId,
- legacyHitsTotal: false,
- });
- isSyncClustered = isTotalHitsGreaterThan(
- (resp.hits.total as unknown) as TotalHits,
- maxResultWindow
- );
- const countData = { isSyncClustered } as CountData;
- syncContext.stopLoading(dataRequestId, requestToken, countData, searchFilters);
+ isSyncClustered = !(await this._documentSource.canLoadAllDocuments(
+ searchFilters,
+ syncContext.registerCancelCallback.bind(null, requestToken)
+ ));
+ syncContext.stopLoading(dataRequestId, requestToken, { isSyncClustered }, searchFilters);
} catch (error) {
if (!(error instanceof DataRequestAbortError) || !isSearchSourceAbortError(error)) {
syncContext.onLoadError(dataRequestId, requestToken, error.message);
diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts
index 368ff8bebcdd1..d12c8432a4191 100644
--- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts
+++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts
@@ -111,6 +111,9 @@ export class HeatmapLayer extends AbstractLayer {
},
syncContext,
source: this.getSource(),
+ getUpdateDueToTimeslice: () => {
+ return true;
+ },
});
} catch (error) {
if (!(error instanceof DataRequestAbortError)) {
diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx
index be113ab4cc2c9..ef41c157a2b17 100644
--- a/x-pack/plugins/maps/public/classes/layers/layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx
@@ -36,6 +36,7 @@ import {
LayerDescriptor,
MapExtent,
StyleDescriptor,
+ Timeslice,
} from '../../../common/descriptor_types';
import { ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source';
import { DataRequestContext } from '../../actions';
@@ -78,7 +79,7 @@ export interface ILayer {
getMbLayerIds(): string[];
ownsMbLayerId(mbLayerId: string): boolean;
ownsMbSourceId(mbSourceId: string): boolean;
- syncLayerWithMB(mbMap: MbMap): void;
+ syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice): void;
getLayerTypeIconName(): string;
isInitialDataLoadComplete(): boolean;
getIndexPatternIds(): string[];
diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
index 6dba935ccc87d..2ad6a5ef73c6d 100644
--- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
@@ -21,6 +21,7 @@ import { VectorLayer, VectorLayerArguments } from '../vector_layer';
import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source';
import { DataRequestContext } from '../../../actions';
import {
+ Timeslice,
VectorLayerDescriptor,
VectorSourceRequestMeta,
} from '../../../../common/descriptor_types';
@@ -66,7 +67,7 @@ export class TiledVectorLayer extends VectorLayer {
dataFilters,
}: DataRequestContext) {
const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_REQUEST_ID}`);
- const searchFilters: VectorSourceRequestMeta = this._getSearchFilters(
+ const searchFilters: VectorSourceRequestMeta = await this._getSearchFilters(
dataFilters,
this.getSource(),
this._style as IVectorStyle
@@ -84,6 +85,10 @@ export class TiledVectorLayer extends VectorLayer {
source: this.getSource(),
prevDataRequest,
nextMeta: searchFilters,
+ getUpdateDueToTimeslice: (timeslice?: Timeslice) => {
+ // TODO use meta features to determine if tiles already contain features for timeslice.
+ return true;
+ },
});
const canSkip = noChangesInSourceState && noChangesInSearchState;
if (canSkip) {
diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx
index d305bb920b2ad..346e59f60af32 100644
--- a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx
@@ -13,7 +13,13 @@ import {
SOURCE_DATA_REQUEST_ID,
VECTOR_SHAPE_TYPE,
} from '../../../../common/constants';
-import { MapExtent, MapQuery, VectorSourceRequestMeta } from '../../../../common/descriptor_types';
+import {
+ DataMeta,
+ MapExtent,
+ MapQuery,
+ Timeslice,
+ VectorSourceRequestMeta,
+} from '../../../../common/descriptor_types';
import { DataRequestContext } from '../../../actions';
import { IVectorSource } from '../../sources/vector_source';
import { DataRequestAbortError } from '../../util/data_request';
@@ -52,6 +58,7 @@ export async function syncVectorSource({
requestMeta,
syncContext,
source,
+ getUpdateDueToTimeslice,
}: {
layerId: string;
layerName: string;
@@ -59,6 +66,7 @@ export async function syncVectorSource({
requestMeta: VectorSourceRequestMeta;
syncContext: DataRequestContext;
source: IVectorSource;
+ getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean;
}): Promise<{ refreshed: boolean; featureCollection: FeatureCollection }> {
const {
startLoading,
@@ -76,6 +84,7 @@ export async function syncVectorSource({
prevDataRequest,
nextMeta: requestMeta,
extentAware: source.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
if (canSkipFetch) {
return {
@@ -104,7 +113,14 @@ export async function syncVectorSource({
) {
layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection));
}
- stopLoading(dataRequestId, requestToken, layerFeatureCollection, meta);
+ const responseMeta: DataMeta = meta ? { ...meta } : {};
+ if (requestMeta.applyGlobalTime && (await source.isTimeAware())) {
+ const timesiceMaskField = await source.getTimesliceMaskFieldName();
+ if (timesiceMaskField) {
+ responseMeta.timesiceMaskField = timesiceMaskField;
+ }
+ }
+ stopLoading(dataRequestId, requestToken, layerFeatureCollection, responseMeta);
return {
refreshed: true,
featureCollection: layerFeatureCollection,
diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
index 8b4d25f4612cc..49a0878ef80b2 100644
--- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
@@ -43,16 +43,19 @@ import {
getFillFilterExpression,
getLineFilterExpression,
getPointFilterExpression,
+ TimesliceMaskConfig,
} from '../../util/mb_filter_expressions';
import {
DynamicStylePropertyOptions,
MapFilters,
MapQuery,
+ Timeslice,
VectorJoinSourceRequestMeta,
VectorLayerDescriptor,
VectorSourceRequestMeta,
VectorStyleRequestMeta,
} from '../../../../common/descriptor_types';
+import { ISource } from '../../sources/source';
import { IVectorSource } from '../../sources/vector_source';
import { CustomIconAndTooltipContent, ILayer } from '../layer';
import { InnerJoin } from '../../joins/inner_join';
@@ -347,6 +350,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
prevDataRequest,
nextMeta: searchFilters,
extentAware: false, // join-sources are term-aggs that are spatially unaware (e.g. ESTermSource/TableSource).
+ getUpdateDueToTimeslice: () => {
+ return true;
+ },
});
if (canSkipFetch) {
return {
@@ -389,17 +395,22 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
return await Promise.all(joinSyncs);
}
- _getSearchFilters(
+ async _getSearchFilters(
dataFilters: MapFilters,
source: IVectorSource,
style: IVectorStyle
- ): VectorSourceRequestMeta {
+ ): Promise {
const fieldNames = [
...source.getFieldNames(),
...style.getSourceFieldNames(),
...this.getValidJoins().map((join) => join.getLeftField().getName()),
];
+ const timesliceMaskFieldName = await source.getTimesliceMaskFieldName();
+ if (timesliceMaskFieldName) {
+ fieldNames.push(timesliceMaskFieldName);
+ }
+
const sourceQuery = this.getQuery() as MapQuery;
return {
...dataFilters,
@@ -674,9 +685,12 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
layerId: this.getId(),
layerName: await this.getDisplayName(source),
prevDataRequest: this.getSourceDataRequest(),
- requestMeta: this._getSearchFilters(syncContext.dataFilters, source, style),
+ requestMeta: await this._getSearchFilters(syncContext.dataFilters, source, style),
syncContext,
source,
+ getUpdateDueToTimeslice: (timeslice?: Timeslice) => {
+ return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice);
+ },
});
await this._syncSupportsFeatureEditing({ syncContext, source });
if (
@@ -754,7 +768,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
}
}
- _setMbPointsProperties(mbMap: MbMap, mvtSourceLayer?: string) {
+ _setMbPointsProperties(
+ mbMap: MbMap,
+ mvtSourceLayer?: string,
+ timesliceMaskConfig?: TimesliceMaskConfig
+ ) {
const pointLayerId = this._getMbPointLayerId();
const symbolLayerId = this._getMbSymbolLayerId();
const pointLayer = mbMap.getLayer(pointLayerId);
@@ -771,7 +789,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
if (symbolLayer) {
mbMap.setLayoutProperty(symbolLayerId, 'visibility', 'none');
}
- this._setMbCircleProperties(mbMap, mvtSourceLayer);
+ this._setMbCircleProperties(mbMap, mvtSourceLayer, timesliceMaskConfig);
} else {
markerLayerId = symbolLayerId;
textLayerId = symbolLayerId;
@@ -779,7 +797,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.setLayoutProperty(pointLayerId, 'visibility', 'none');
mbMap.setLayoutProperty(this._getMbTextLayerId(), 'visibility', 'none');
}
- this._setMbSymbolProperties(mbMap, mvtSourceLayer);
+ this._setMbSymbolProperties(mbMap, mvtSourceLayer, timesliceMaskConfig);
}
this.syncVisibilityWithMb(mbMap, markerLayerId);
@@ -790,7 +808,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
}
}
- _setMbCircleProperties(mbMap: MbMap, mvtSourceLayer?: string) {
+ _setMbCircleProperties(
+ mbMap: MbMap,
+ mvtSourceLayer?: string,
+ timesliceMaskConfig?: TimesliceMaskConfig
+ ) {
const sourceId = this.getId();
const pointLayerId = this._getMbPointLayerId();
const pointLayer = mbMap.getLayer(pointLayerId);
@@ -822,7 +844,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.addLayer(mbLayer);
}
- const filterExpr = getPointFilterExpression(this.hasJoins());
+ const filterExpr = getPointFilterExpression(this.hasJoins(), timesliceMaskConfig);
if (!_.isEqual(filterExpr, mbMap.getFilter(pointLayerId))) {
mbMap.setFilter(pointLayerId, filterExpr);
mbMap.setFilter(textLayerId, filterExpr);
@@ -841,7 +863,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
});
}
- _setMbSymbolProperties(mbMap: MbMap, mvtSourceLayer?: string) {
+ _setMbSymbolProperties(
+ mbMap: MbMap,
+ mvtSourceLayer?: string,
+ timesliceMaskConfig?: TimesliceMaskConfig
+ ) {
const sourceId = this.getId();
const symbolLayerId = this._getMbSymbolLayerId();
const symbolLayer = mbMap.getLayer(symbolLayerId);
@@ -858,7 +884,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.addLayer(mbLayer);
}
- const filterExpr = getPointFilterExpression(this.hasJoins());
+ const filterExpr = getPointFilterExpression(this.hasJoins(), timesliceMaskConfig);
if (!_.isEqual(filterExpr, mbMap.getFilter(symbolLayerId))) {
mbMap.setFilter(symbolLayerId, filterExpr);
}
@@ -876,7 +902,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
});
}
- _setMbLinePolygonProperties(mbMap: MbMap, mvtSourceLayer?: string) {
+ _setMbLinePolygonProperties(
+ mbMap: MbMap,
+ mvtSourceLayer?: string,
+ timesliceMaskConfig?: TimesliceMaskConfig
+ ) {
const sourceId = this.getId();
const fillLayerId = this._getMbPolygonLayerId();
const lineLayerId = this._getMbLineLayerId();
@@ -940,14 +970,14 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
this.syncVisibilityWithMb(mbMap, fillLayerId);
mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom());
- const fillFilterExpr = getFillFilterExpression(hasJoins);
+ const fillFilterExpr = getFillFilterExpression(hasJoins, timesliceMaskConfig);
if (!_.isEqual(fillFilterExpr, mbMap.getFilter(fillLayerId))) {
mbMap.setFilter(fillLayerId, fillFilterExpr);
}
this.syncVisibilityWithMb(mbMap, lineLayerId);
mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom());
- const lineFilterExpr = getLineFilterExpression(hasJoins);
+ const lineFilterExpr = getLineFilterExpression(hasJoins, timesliceMaskConfig);
if (!_.isEqual(lineFilterExpr, mbMap.getFilter(lineLayerId))) {
mbMap.setFilter(lineLayerId, lineFilterExpr);
}
@@ -956,7 +986,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom());
}
- _setMbCentroidProperties(mbMap: MbMap, mvtSourceLayer?: string) {
+ _setMbCentroidProperties(
+ mbMap: MbMap,
+ mvtSourceLayer?: string,
+ timesliceMaskConfig?: TimesliceMaskConfig
+ ) {
const centroidLayerId = this._getMbCentroidLayerId();
const centroidLayer = mbMap.getLayer(centroidLayerId);
if (!centroidLayer) {
@@ -971,7 +1005,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.addLayer(mbLayer);
}
- const filterExpr = getCentroidFilterExpression(this.hasJoins());
+ const filterExpr = getCentroidFilterExpression(this.hasJoins(), timesliceMaskConfig);
if (!_.isEqual(filterExpr, mbMap.getFilter(centroidLayerId))) {
mbMap.setFilter(centroidLayerId, filterExpr);
}
@@ -986,17 +1020,32 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.setLayerZoomRange(centroidLayerId, this.getMinZoom(), this.getMaxZoom());
}
- _syncStylePropertiesWithMb(mbMap: MbMap) {
- this._setMbPointsProperties(mbMap);
- this._setMbLinePolygonProperties(mbMap);
+ _syncStylePropertiesWithMb(mbMap: MbMap, timeslice?: Timeslice) {
+ const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice);
+ this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig);
+ this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig);
// centroid layers added after polygon layers to ensure they are on top of polygon layers
- this._setMbCentroidProperties(mbMap);
+ this._setMbCentroidProperties(mbMap, undefined, timesliceMaskConfig);
}
- syncLayerWithMB(mbMap: MbMap) {
+ _getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined {
+ if (!timeslice || this.hasJoins()) {
+ return;
+ }
+
+ const prevMeta = this.getSourceDataRequest()?.getMeta();
+ return prevMeta !== undefined && prevMeta.timesiceMaskField !== undefined
+ ? {
+ timesiceMaskField: prevMeta.timesiceMaskField,
+ timeslice,
+ }
+ : undefined;
+ }
+
+ syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice) {
addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap);
this._syncFeatureCollectionWithMb(mbMap);
- this._syncStylePropertiesWithMb(mbMap);
+ this._syncStylePropertiesWithMb(mbMap, timeslice);
}
_getMbPointLayerId() {
@@ -1094,6 +1143,15 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
return await this._source.getLicensedFeatures();
}
+ _getUpdateDueToTimesliceFromSourceRequestMeta(source: ISource, timeslice?: Timeslice) {
+ const prevDataRequest = this.getSourceDataRequest();
+ const prevMeta = prevDataRequest?.getMeta();
+ if (!prevMeta) {
+ return true;
+ }
+ return source.getUpdateDueToTimeslice(prevMeta, timeslice);
+ }
+
async addFeature(geometry: Geometry | Position[]) {
const layerSource = this.getSource();
await layerSource.addFeature(geometry);
diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx
index a51e291574b70..9f7bd1260ca22 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx
@@ -12,13 +12,19 @@ import { i18n } from '@kbn/i18n';
import { IFieldType, IndexPattern } from 'src/plugins/data/public';
import { GeoJsonProperties, Geometry, Position } from 'geojson';
import { AbstractESSource } from '../es_source';
-import { getHttp, getMapAppConfig, getSearchService } from '../../../kibana_services';
+import {
+ getHttp,
+ getMapAppConfig,
+ getSearchService,
+ getTimeFilter,
+} from '../../../kibana_services';
import {
addFieldToDSL,
getField,
hitsToGeoJson,
isTotalHitsGreaterThan,
PreIndexedShape,
+ TotalHits,
} from '../../../../common/elasticsearch_util';
// @ts-expect-error
import { UpdateSourceEditor } from './update_source_editor';
@@ -41,11 +47,14 @@ import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
import { ESDocField } from '../../fields/es_doc_field';
import { registerSource } from '../source_registry';
import {
+ DataMeta,
ESSearchSourceDescriptor,
+ Timeslice,
VectorSourceRequestMeta,
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters';
+import { TimeRange } from '../../../../../../../src/plugins/data/common';
import { ImmutableSourceProperty, SourceEditorArgs } from '../source';
import { IField } from '../../fields/field';
import { GeoJsonWithMeta, SourceTooltipConfig } from '../vector_source';
@@ -59,6 +68,16 @@ import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_sou
import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source';
import { addFeatureToIndex, getMatchingIndexes } from './util/feature_edit';
+export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined {
+ const timeRangeBounds = getTimeFilter().calculateBounds(timerange);
+ return timeRangeBounds.min !== undefined && timeRangeBounds.max !== undefined
+ ? {
+ from: timeRangeBounds.min.valueOf(),
+ to: timeRangeBounds.max.valueOf(),
+ }
+ : undefined;
+}
+
export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', {
defaultMessage: 'Documents',
});
@@ -338,7 +357,6 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
async _getSearchHits(
layerName: string,
searchFilters: VectorSourceRequestMeta,
- maxResultWindow: number,
registerCancelCallback: (callback: () => void) => void
) {
const indexPattern = await this.getIndexPattern();
@@ -350,8 +368,18 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
);
const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source
+
+ // Use Kibana global time extent instead of timeslice extent when all documents for global time extent can be loaded
+ // to allow for client-side masking of timeslice
+ const searchFiltersWithoutTimeslice = { ...searchFilters };
+ delete searchFiltersWithoutTimeslice.timeslice;
+ const useSearchFiltersWithoutTimeslice =
+ searchFilters.timeslice !== undefined &&
+ (await this.canLoadAllDocuments(searchFiltersWithoutTimeslice, registerCancelCallback));
+
+ const maxResultWindow = await this.getMaxResultWindow();
const searchSource = await this.makeSearchSource(
- searchFilters,
+ useSearchFiltersWithoutTimeslice ? searchFiltersWithoutTimeslice : searchFilters,
maxResultWindow,
initialSearchContext
);
@@ -375,11 +403,17 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
searchSessionId: searchFilters.searchSessionId,
});
+ const isTimeExtentForTimeslice =
+ searchFilters.timeslice !== undefined && !useSearchFiltersWithoutTimeslice;
return {
hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top
meta: {
resultsCount: resp.hits.hits.length,
areResultsTrimmed: isTotalHitsGreaterThan(resp.hits.total, resp.hits.hits.length),
+ timeExtent: isTimeExtentForTimeslice
+ ? searchFilters.timeslice
+ : timerangeToTimeextent(searchFilters.timeFilters),
+ isTimeExtentForTimeslice,
},
};
}
@@ -424,16 +458,9 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
): Promise {
const indexPattern = await this.getIndexPattern();
- const indexSettings = await loadIndexSettings(indexPattern.title);
-
const { hits, meta } = this._isTopHits()
? await this._getTopHits(layerName, searchFilters, registerCancelCallback)
- : await this._getSearchHits(
- layerName,
- searchFilters,
- indexSettings.maxResultWindow,
- registerCancelCallback
- );
+ : await this._getSearchHits(layerName, searchFilters, registerCancelCallback);
const unusedMetaFields = indexPattern.metaFields.filter((metaField) => {
return !['_id', '_index'].includes(metaField);
@@ -743,6 +770,62 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
: urlTemplate,
};
}
+
+ async getTimesliceMaskFieldName(): Promise {
+ if (this._isTopHits() || this._descriptor.scalingType === SCALING_TYPES.MVT) {
+ return null;
+ }
+
+ const indexPattern = await this.getIndexPattern();
+ return indexPattern.timeFieldName ? indexPattern.timeFieldName : null;
+ }
+
+ getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean {
+ if (this._isTopHits() || this._descriptor.scalingType === SCALING_TYPES.MVT) {
+ return true;
+ }
+
+ if (
+ prevMeta.timeExtent === undefined ||
+ prevMeta.areResultsTrimmed === undefined ||
+ prevMeta.areResultsTrimmed
+ ) {
+ return true;
+ }
+
+ const isTimeExtentForTimeslice =
+ prevMeta.isTimeExtentForTimeslice !== undefined ? prevMeta.isTimeExtentForTimeslice : false;
+ if (!timeslice) {
+ return isTimeExtentForTimeslice
+ ? // Previous request only covers timeslice extent. Will need to re-fetch data to cover global time extent
+ true
+ : // Previous request covers global time extent.
+ // No need to re-fetch data since previous request already has data for the entire global time extent.
+ false;
+ }
+
+ const isWithin = isTimeExtentForTimeslice
+ ? timeslice.from >= prevMeta.timeExtent.from && timeslice.to <= prevMeta.timeExtent.to
+ : true;
+ return !isWithin;
+ }
+
+ async canLoadAllDocuments(
+ searchFilters: VectorSourceRequestMeta,
+ registerCancelCallback: (callback: () => void) => void
+ ) {
+ const abortController = new AbortController();
+ registerCancelCallback(() => abortController.abort());
+ const maxResultWindow = await this.getMaxResultWindow();
+ const searchSource = await this.makeSearchSource(searchFilters, 0);
+ searchSource.setField('trackTotalHits', maxResultWindow + 1);
+ const resp = await searchSource.fetch({
+ abortSignal: abortController.signal,
+ sessionId: searchFilters.searchSessionId,
+ legacyHitsTotal: false,
+ });
+ return !isTotalHitsGreaterThan((resp.hits.total as unknown) as TotalHits, maxResultWindow);
+ }
}
registerSource({
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
index d58e71db2a9ab..5bf7a2e47cc66 100644
--- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
@@ -228,6 +228,10 @@ export class MVTSingleLayerVectorSource
return tooltips;
}
+ async getTimesliceMaskFieldName() {
+ return null;
+ }
+
async supportsFeatureEditing(): Promise {
return false;
}
diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts
index 7a8fca337fd2e..0ecbde06cf3e2 100644
--- a/x-pack/plugins/maps/public/classes/sources/source.ts
+++ b/x-pack/plugins/maps/public/classes/sources/source.ts
@@ -13,7 +13,12 @@ import { GeoJsonProperties } from 'geojson';
import { copyPersistentState } from '../../reducers/copy_persistent_state';
import { IField } from '../fields/field';
import { FieldFormatter, LAYER_TYPE, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
-import { AbstractSourceDescriptor, Attribution } from '../../../common/descriptor_types';
+import {
+ AbstractSourceDescriptor,
+ Attribution,
+ DataMeta,
+ Timeslice,
+} from '../../../common/descriptor_types';
import { LICENSED_FEATURES } from '../../licensed_features';
import { PreIndexedShape } from '../../../common/elasticsearch_util';
@@ -64,6 +69,7 @@ export interface ISource {
getMinZoom(): number;
getMaxZoom(): number;
getLicensedFeatures(): Promise;
+ getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean;
}
export class AbstractSource implements ISource {
@@ -194,4 +200,8 @@ export class AbstractSource implements ISource {
async getLicensedFeatures(): Promise {
return [];
}
+
+ getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean {
+ return true;
+ }
}
diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx
index 1194d571e344b..8f93de705e365 100644
--- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx
@@ -66,6 +66,7 @@ export interface IVectorSource extends ISource {
getSupportedShapeTypes(): Promise;
isBoundsAware(): boolean;
getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig;
+ getTimesliceMaskFieldName(): Promise;
supportsFeatureEditing(): Promise;
addFeature(geometry: Geometry | Position[]): Promise;
}
@@ -156,6 +157,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
return null;
}
+ async getTimesliceMaskFieldName(): Promise {
+ return null;
+ }
+
async addFeature(geometry: Geometry | Position[]) {
throw new Error('Should implement VectorSource#addFeature');
}
diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js
index c13b2fd441cad..da3cbb9055d43 100644
--- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js
+++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js
@@ -82,6 +82,9 @@ describe('updateDueToExtent', () => {
describe('canSkipSourceUpdate', () => {
const SOURCE_DATA_REQUEST_ID = 'foo';
+ const getUpdateDueToTimeslice = () => {
+ return true;
+ };
describe('isQueryAware', () => {
const queryAwareSourceMock = {
@@ -136,6 +139,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -156,6 +160,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -176,6 +181,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -193,6 +199,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -224,6 +231,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -244,6 +252,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -264,6 +273,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -281,6 +291,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -327,6 +338,7 @@ describe('canSkipSourceUpdate', () => {
applyGlobalTime: false,
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -346,6 +358,7 @@ describe('canSkipSourceUpdate', () => {
applyGlobalTime: true,
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -375,6 +388,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -402,6 +416,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -429,6 +444,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -463,6 +479,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -498,6 +515,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -529,6 +547,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -564,6 +583,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -599,6 +619,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts
index 1f2678f40eecd..b6f03ef3d1c63 100644
--- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts
+++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts
@@ -10,7 +10,7 @@ import turfBboxPolygon from '@turf/bbox-polygon';
import turfBooleanContains from '@turf/boolean-contains';
import { isRefreshOnlyQuery } from './is_refresh_only_query';
import { ISource } from '../sources/source';
-import { DataMeta } from '../../../common/descriptor_types';
+import { DataMeta, Timeslice } from '../../../common/descriptor_types';
import { DataRequest } from './data_request';
const SOURCE_UPDATE_REQUIRED = true;
@@ -56,11 +56,13 @@ export async function canSkipSourceUpdate({
prevDataRequest,
nextMeta,
extentAware,
+ getUpdateDueToTimeslice,
}: {
source: ISource;
prevDataRequest: DataRequest | undefined;
nextMeta: DataMeta;
extentAware: boolean;
+ getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean;
}): Promise {
const timeAware = await source.isTimeAware();
const refreshTimerAware = await source.isRefreshTimerAware();
@@ -94,7 +96,9 @@ export async function canSkipSourceUpdate({
updateDueToApplyGlobalTime = prevMeta.applyGlobalTime !== nextMeta.applyGlobalTime;
if (nextMeta.applyGlobalTime) {
updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters);
- updateDueToTimeslice = !_.isEqual(prevMeta.timeslice, nextMeta.timeslice);
+ if (!_.isEqual(prevMeta.timeslice, nextMeta.timeslice)) {
+ updateDueToTimeslice = getUpdateDueToTimeslice(nextMeta.timeslice);
+ }
}
}
diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts
index f5df741759cb3..6a193216c7c1e 100644
--- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts
+++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts
@@ -12,67 +12,110 @@ import {
KBN_TOO_MANY_FEATURES_PROPERTY,
} from '../../../common/constants';
+import { Timeslice } from '../../../common/descriptor_types';
+
+export interface TimesliceMaskConfig {
+ timesiceMaskField: string;
+ timeslice: Timeslice;
+}
+
export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true];
const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true];
-function getFilterExpression(geometryFilter: unknown[], hasJoins: boolean) {
- const filters: unknown[] = [
- EXCLUDE_TOO_MANY_FEATURES_BOX,
- EXCLUDE_CENTROID_FEATURES,
- geometryFilter,
- ];
+function getFilterExpression(
+ filters: unknown[],
+ hasJoins: boolean,
+ timesliceMaskConfig?: TimesliceMaskConfig
+) {
+ const allFilters: unknown[] = [...filters];
if (hasJoins) {
- filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]);
+ allFilters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]);
}
- return ['all', ...filters];
+ if (timesliceMaskConfig) {
+ allFilters.push(['has', timesliceMaskConfig.timesiceMaskField]);
+ allFilters.push([
+ '>=',
+ ['get', timesliceMaskConfig.timesiceMaskField],
+ timesliceMaskConfig.timeslice.from,
+ ]);
+ allFilters.push([
+ '<',
+ ['get', timesliceMaskConfig.timesiceMaskField],
+ timesliceMaskConfig.timeslice.to,
+ ]);
+ }
+
+ return ['all', ...allFilters];
}
-export function getFillFilterExpression(hasJoins: boolean): unknown[] {
+export function getFillFilterExpression(
+ hasJoins: boolean,
+ timesliceMaskConfig?: TimesliceMaskConfig
+): unknown[] {
return getFilterExpression(
[
- 'any',
- ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
+ EXCLUDE_TOO_MANY_FEATURES_BOX,
+ EXCLUDE_CENTROID_FEATURES,
+ [
+ 'any',
+ ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
+ ],
],
- hasJoins
+ hasJoins,
+ timesliceMaskConfig
);
}
-export function getLineFilterExpression(hasJoins: boolean): unknown[] {
+export function getLineFilterExpression(
+ hasJoins: boolean,
+ timesliceMaskConfig?: TimesliceMaskConfig
+): unknown[] {
return getFilterExpression(
[
- 'any',
- ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
- ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING],
+ EXCLUDE_TOO_MANY_FEATURES_BOX,
+ EXCLUDE_CENTROID_FEATURES,
+ [
+ 'any',
+ ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING],
+ ],
],
- hasJoins
+ hasJoins,
+ timesliceMaskConfig
);
}
-export function getPointFilterExpression(hasJoins: boolean): unknown[] {
+export function getPointFilterExpression(
+ hasJoins: boolean,
+ timesliceMaskConfig?: TimesliceMaskConfig
+): unknown[] {
return getFilterExpression(
[
- 'any',
- ['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT],
+ EXCLUDE_TOO_MANY_FEATURES_BOX,
+ EXCLUDE_CENTROID_FEATURES,
+ [
+ 'any',
+ ['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT],
+ ],
],
- hasJoins
+ hasJoins,
+ timesliceMaskConfig
);
}
-export function getCentroidFilterExpression(hasJoins: boolean): unknown[] {
- const filters: unknown[] = [
- EXCLUDE_TOO_MANY_FEATURES_BOX,
- ['==', ['get', KBN_IS_CENTROID_FEATURE], true],
- ];
-
- if (hasJoins) {
- filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]);
- }
-
- return ['all', ...filters];
+export function getCentroidFilterExpression(
+ hasJoins: boolean,
+ timesliceMaskConfig?: TimesliceMaskConfig
+): unknown[] {
+ return getFilterExpression(
+ [EXCLUDE_TOO_MANY_FEATURES_BOX, ['==', ['get', KBN_IS_CENTROID_FEATURE], true]],
+ hasJoins,
+ timesliceMaskConfig
+ );
}
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts
index f0df797582bef..998329a78bfbb 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts
@@ -11,7 +11,11 @@
import turfDistance from '@turf/distance';
// @ts-expect-error
import turfCircle from '@turf/circle';
-import { Position } from 'geojson';
+import { Feature, GeoJSON, Position } from 'geojson';
+
+const DRAW_CIRCLE_RADIUS = 'draw-circle-radius';
+
+export const DRAW_CIRCLE_RADIUS_MB_FILTER = ['==', 'meta', DRAW_CIRCLE_RADIUS];
export interface DrawCircleProperties {
center: Position;
@@ -22,10 +26,12 @@ type DrawCircleState = {
circle: {
properties: Omit & {
center: Position | null;
+ edge: Position | null;
+ radiusKm: number;
};
id: string | number;
incomingCoords: (coords: unknown[]) => void;
- toGeoJSON: () => unknown;
+ toGeoJSON: () => GeoJSON;
};
};
@@ -43,6 +49,7 @@ export const DrawCircle = {
type: 'Feature',
properties: {
center: null,
+ edge: null,
radiusKm: 0,
},
geometry: {
@@ -96,6 +103,7 @@ export const DrawCircle = {
}
const mouseLocation = [e.lngLat.lng, e.lngLat.lat];
+ state.circle.properties.edge = mouseLocation;
state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, mouseLocation);
const newCircleFeature = turfCircle(
state.circle.properties.center,
@@ -124,15 +132,53 @@ export const DrawCircle = {
this.changeMode('simple_select', {}, { silent: true });
}
},
- toDisplayFeatures(
- state: DrawCircleState,
- geojson: { properties: { active: string } },
- display: (geojson: unknown) => unknown
- ) {
- if (state.circle.properties.center) {
- geojson.properties.active = 'true';
- return display(geojson);
+ toDisplayFeatures(state: DrawCircleState, geojson: Feature, display: (geojson: Feature) => void) {
+ if (!state.circle.properties.center || !state.circle.properties.edge) {
+ return null;
+ }
+
+ geojson.properties!.active = 'true';
+
+ let radiusLabel = '';
+ if (state.circle.properties.radiusKm <= 1) {
+ radiusLabel = `${Math.round(state.circle.properties.radiusKm * 1000)} m`;
+ } else if (state.circle.properties.radiusKm <= 10) {
+ radiusLabel = `${state.circle.properties.radiusKm.toFixed(1)} km`;
+ } else {
+ radiusLabel = `${Math.round(state.circle.properties.radiusKm)} km`;
}
+
+ // display radius label, requires custom 'symbol' style with DRAW_CIRCLE_RADIUS_MB_FILTER filter
+ display({
+ type: 'Feature',
+ properties: {
+ meta: DRAW_CIRCLE_RADIUS,
+ parent: state.circle.id,
+ radiusLabel,
+ active: 'false',
+ },
+ geometry: {
+ type: 'Point',
+ coordinates: state.circle.properties.edge,
+ },
+ });
+
+ // display line from center vertex to edge
+ display({
+ type: 'Feature',
+ properties: {
+ meta: 'draw-circle-radius-line',
+ parent: state.circle.id,
+ active: 'true',
+ },
+ geometry: {
+ type: 'LineString',
+ coordinates: [state.circle.properties.center, state.circle.properties.edge],
+ },
+ });
+
+ // display circle
+ display(geojson);
},
onTrash(state: DrawCircleState) {
// @ts-ignore
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx
index 879bd85dd6019..5d9cb59bbe522 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx
@@ -14,9 +14,11 @@ import DrawRectangle from 'mapbox-gl-draw-rectangle-mode';
import type { Map as MbMap } from '@kbn/mapbox-gl';
import { Feature } from 'geojson';
import { DRAW_SHAPE } from '../../../../common/constants';
-import { DrawCircle } from './draw_circle';
+import { DrawCircle, DRAW_CIRCLE_RADIUS_MB_FILTER } from './draw_circle';
import { DrawTooltip } from './draw_tooltip';
+const GL_DRAW_RADIUS_LABEL_LAYER_ID = 'gl-draw-radius-label';
+
const mbModeEquivalencies = new Map([
['simple_select', DRAW_SHAPE.SIMPLE_SELECT],
['draw_rectangle', DRAW_SHAPE.BOUNDS],
@@ -94,6 +96,7 @@ export class DrawControl extends Component {
this.props.mbMap.getCanvas().style.cursor = '';
this.props.mbMap.off('draw.modechange', this._onModeChange);
this.props.mbMap.off('draw.create', this._onDraw);
+ this.props.mbMap.removeLayer(GL_DRAW_RADIUS_LABEL_LAYER_ID);
this.props.mbMap.removeControl(this._mbDrawControl);
this._mbDrawControlAdded = false;
}
@@ -105,6 +108,25 @@ export class DrawControl extends Component {
if (!this._mbDrawControlAdded) {
this.props.mbMap.addControl(this._mbDrawControl);
+ this.props.mbMap.addLayer({
+ id: GL_DRAW_RADIUS_LABEL_LAYER_ID,
+ type: 'symbol',
+ source: 'mapbox-gl-draw-hot',
+ filter: DRAW_CIRCLE_RADIUS_MB_FILTER,
+ layout: {
+ 'text-anchor': 'right',
+ 'text-field': '{radiusLabel}',
+ 'text-size': 16,
+ 'text-offset': [-1, 0],
+ 'text-ignore-placement': true,
+ 'text-allow-overlap': true,
+ },
+ paint: {
+ 'text-color': '#fbb03b',
+ 'text-halo-color': 'rgba(255, 255, 255, 1)',
+ 'text-halo-width': 2,
+ },
+ });
this._mbDrawControlAdded = true;
this.props.mbMap.getCanvas().style.cursor = 'crosshair';
this.props.mbMap.on('draw.modechange', this._onModeChange);
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts
index 4f94cbc7b7458..b9b4b184318f5 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts
@@ -27,6 +27,7 @@ import {
getMapSettings,
getScrollZoom,
getSpatialFiltersLayer,
+ getTimeslice,
} from '../../selectors/map_selectors';
import { getDrawMode, getIsFullScreen } from '../../selectors/ui_selectors';
import { getInspectorAdapters } from '../../reducers/non_serializable_instances';
@@ -43,6 +44,7 @@ function mapStateToProps(state: MapStoreState) {
inspectorAdapters: getInspectorAdapters(state),
scrollZoom: getScrollZoom(state),
isFullScreen: getIsFullScreen(state),
+ timeslice: getTimeslice(state),
featureModeActive:
getDrawMode(state) === DRAW_MODE.DRAW_SHAPES || getDrawMode(state) === DRAW_MODE.DRAW_POINTS,
filterModeActive: getDrawMode(state) === DRAW_MODE.DRAW_FILTERS,
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx
index 96ff7b7dcf882..2ce4e2d98ce5f 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx
@@ -25,7 +25,7 @@ import { getInitialView } from './get_initial_view';
import { getPreserveDrawingBuffer } from '../../kibana_services';
import { ILayer } from '../../classes/layers/layer';
import { MapSettings } from '../../reducers/map';
-import { Goto, MapCenterAndZoom } from '../../../common/descriptor_types';
+import { Goto, MapCenterAndZoom, Timeslice } from '../../../common/descriptor_types';
import {
DECIMAL_DEGREES_PRECISION,
KBN_TOO_MANY_FEATURES_IMAGE_ID,
@@ -68,13 +68,12 @@ export interface Props {
onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void;
renderTooltipContent?: RenderToolTipContent;
setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void;
+ timeslice?: Timeslice;
featureModeActive: boolean;
filterModeActive: boolean;
}
interface State {
- prevLayerList: ILayer[] | undefined;
- hasSyncedLayerList: boolean;
mbMap: MapboxMap | undefined;
}
@@ -83,38 +82,23 @@ export class MBMap extends Component {
private _isMounted: boolean = false;
private _containerRef: HTMLDivElement | null = null;
private _prevDisableInteractive?: boolean;
+ private _prevLayerList?: ILayer[];
+ private _prevTimeslice?: Timeslice;
private _navigationControl = new mapboxgl.NavigationControl({ showCompass: false });
private _tileStatusTracker?: TileStatusTracker;
state: State = {
- prevLayerList: undefined,
- hasSyncedLayerList: false,
mbMap: undefined,
};
- static getDerivedStateFromProps(nextProps: Props, prevState: State) {
- const nextLayerList = nextProps.layerList;
- if (nextLayerList !== prevState.prevLayerList) {
- return {
- prevLayerList: nextLayerList,
- hasSyncedLayerList: false,
- };
- }
-
- return null;
- }
-
componentDidMount() {
this._initializeMap();
this._isMounted = true;
}
componentDidUpdate() {
- if (this.state.mbMap) {
- // do not debounce syncing of map-state
- this._syncMbMapWithMapState();
- this._debouncedSync();
- }
+ this._syncMbMapWithMapState(); // do not debounce syncing of map-state
+ this._debouncedSync();
}
componentWillUnmount() {
@@ -134,16 +118,13 @@ export class MBMap extends Component {
_debouncedSync = _.debounce(() => {
if (this._isMounted && this.props.isMapReady && this.state.mbMap) {
- if (!this.state.hasSyncedLayerList) {
- this.setState(
- {
- hasSyncedLayerList: true,
- },
- () => {
- this._syncMbMapWithLayerList();
- this._syncMbMapWithInspector();
- }
- );
+ const hasLayerListChanged = this._prevLayerList !== this.props.layerList; // Comparing re-select memoized instance so no deep equals needed
+ const hasTimesliceChanged = !_.isEqual(this._prevTimeslice, this.props.timeslice);
+ if (hasLayerListChanged || hasTimesliceChanged) {
+ this._prevLayerList = this.props.layerList;
+ this._prevTimeslice = this.props.timeslice;
+ this._syncMbMapWithLayerList();
+ this._syncMbMapWithInspector();
}
this.props.spatialFiltersLayer.syncLayerWithMB(this.state.mbMap);
this._syncSettings();
@@ -346,7 +327,9 @@ export class MBMap extends Component {
this.props.layerList,
this.props.spatialFiltersLayer
);
- this.props.layerList.forEach((layer) => layer.syncLayerWithMB(this.state.mbMap!));
+ this.props.layerList.forEach((layer) =>
+ layer.syncLayerWithMB(this.state.mbMap!, this.props.timeslice)
+ );
syncLayerOrder(this.state.mbMap, this.props.spatialFiltersLayer, this.props.layerList);
};
diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
index 5a477754683e6..509cece671dd6 100644
--- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
+++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
@@ -54,9 +54,9 @@ import {
} from '../selectors/map_selectors';
import {
APP_ID,
- getExistingMapPath,
+ getEditPath,
+ getFullPath,
MAP_SAVED_OBJECT_TYPE,
- MAP_PATH,
RawValue,
} from '../../common/constants';
import { RenderToolTipContent } from '../classes/tooltips/tooltip_property';
@@ -180,13 +180,13 @@ export class MapEmbeddable
: '';
const input = this.getInput();
const title = input.hidePanelTitles ? '' : input.title || savedMapTitle;
- const savedObjectId = (input as MapByReferenceInput).savedObjectId;
+ const savedObjectId = 'savedObjectId' in input ? input.savedObjectId : undefined;
this.updateOutput({
...this.getOutput(),
defaultTitle: savedMapTitle,
title,
- editPath: `/${MAP_PATH}/${savedObjectId}`,
- editUrl: getHttp().basePath.prepend(getExistingMapPath(savedObjectId)),
+ editPath: getEditPath(savedObjectId),
+ editUrl: getHttp().basePath.prepend(getFullPath(savedObjectId)),
indexPatterns: await this._getIndexPatterns(),
});
}
diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts
index eff49c1b1242e..cc0ed19db0b40 100644
--- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts
+++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts
@@ -6,40 +6,22 @@
*/
import { suggestEMSTermJoinConfig } from './ems_autosuggest';
-import { FeatureCollection } from 'geojson';
class MockFileLayer {
- private readonly _url: string;
private readonly _id: string;
private readonly _fields: Array<{ id: string }>;
- constructor(url: string, fields: Array<{ id: string }>) {
- this._url = url;
- this._id = url;
+ constructor(id: string, fields: Array<{ id: string; alias?: string[]; values?: string[] }>) {
+ this._id = id;
this._fields = fields;
}
- getFields() {
- return this._fields;
+ getId() {
+ return this._id;
}
- getGeoJson() {
- if (this._url === 'world_countries') {
- return ({
- type: 'FeatureCollection',
- features: [
- { properties: { iso2: 'CA', iso3: 'CAN' } },
- { properties: { iso2: 'US', iso3: 'USA' } },
- ],
- } as unknown) as FeatureCollection;
- } else if (this._url === 'zips') {
- return ({
- type: 'FeatureCollection',
- features: [{ properties: { zip: '40204' } }, { properties: { zip: '40205' } }],
- } as unknown) as FeatureCollection;
- } else {
- throw new Error(`unrecognized mock url ${this._url}`);
- }
+ getFields() {
+ return this._fields;
}
hasId(id: string) {
@@ -51,31 +33,31 @@ jest.mock('../util', () => {
return {
async getEmsFileLayers() {
return [
- new MockFileLayer('world_countries', [{ id: 'iso2' }, { id: 'iso3' }]),
- new MockFileLayer('zips', [{ id: 'zip' }]),
+ new MockFileLayer('world_countries', [
+ {
+ id: 'iso2',
+ alias: ['(geo\\.){0,}country_iso_code$', '(country|countries)'],
+ values: ['CA', 'US'],
+ },
+ { id: 'iso3', values: ['CAN', 'USA'] },
+ { id: 'name', alias: ['(country|countries)'] },
+ ]),
+ new MockFileLayer('usa_zip_codes', [
+ { id: 'zip', alias: ['zip'], values: ['40204', '40205'] },
+ ]),
];
},
};
});
describe('suggestEMSTermJoinConfig', () => {
- test('no info provided', async () => {
+ test('Should not validate when no info provided', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({});
expect(termJoinConfig).toBe(null);
});
- describe('validate common column names', () => {
- test('ecs region', async () => {
- const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValuesColumnName: 'destination.geo.region_iso_code',
- });
- expect(termJoinConfig).toEqual({
- layerId: 'administrative_regions_lvl2',
- field: 'region_iso_code',
- });
- });
-
- test('ecs country', async () => {
+ describe('With common column names', () => {
+ test('should match first match', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
sampleValuesColumnName: 'country_iso_code',
});
@@ -85,78 +67,61 @@ describe('suggestEMSTermJoinConfig', () => {
});
});
- test('country', async () => {
+ test('When sampleValues are provided, should reject match if no sampleValues for a layer, even though the name matches', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValuesColumnName: 'Country_name',
- });
- expect(termJoinConfig).toEqual({
- layerId: 'world_countries',
- field: 'name',
+ sampleValuesColumnName: 'country_iso_code',
+ sampleValues: ['FO', 'US', 'CA'],
});
+ expect(termJoinConfig).toEqual(null);
});
- test('unknown name', async () => {
+ test('should reject match if sampleValues not in id-list', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValuesColumnName: 'cntry',
+ sampleValuesColumnName: 'zip',
+ sampleValues: ['90201', '40205'],
});
expect(termJoinConfig).toEqual(null);
});
- });
- describe('validate well known formats', () => {
- test('5-digit zip code', async () => {
+ test('should return first match (regex matches both iso2 and name)', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['90201', 40204],
+ sampleValuesColumnName: 'Country_name',
});
expect(termJoinConfig).toEqual({
- layerId: 'usa_zip_codes',
- field: 'zip',
+ layerId: 'world_countries',
+ field: 'iso2',
});
});
- test('mismatch', async () => {
+ test('unknown name', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['90201', 'foobar'],
+ sampleValuesColumnName: 'cntry',
});
expect(termJoinConfig).toEqual(null);
});
});
- describe('validate based on EMS data', () => {
- test('Should validate with zip codes layer', async () => {
+ describe('validate well known formats (using id-values in manifest)', () => {
+ test('Should validate known zipcodes', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['40204', 40205],
- emsLayerIds: ['world_countries', 'zips'],
+ sampleValues: ['40205', 40204],
});
expect(termJoinConfig).toEqual({
- layerId: 'zips',
+ layerId: 'usa_zip_codes',
field: 'zip',
});
});
- test('Should not validate with faulty zip codes', async () => {
+ test('Should not validate unknown zipcode (in this case, 90201)', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['40204', '00000'],
- emsLayerIds: ['world_countries', 'zips'],
+ sampleValues: ['90201', 40204],
});
expect(termJoinConfig).toEqual(null);
});
- test('Should validate against countries', async () => {
+ test('Should not validate mismatches', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['USA', 'USA', 'CAN'],
- emsLayerIds: ['world_countries', 'zips'],
- });
- expect(termJoinConfig).toEqual({
- layerId: 'world_countries',
- field: 'iso3',
- });
- });
-
- test('Should not validate against missing countries', async () => {
- const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['USA', 'BEL', 'CAN'],
- emsLayerIds: ['world_countries', 'zips'],
+ sampleValues: ['90201', 'foobar'],
});
expect(termJoinConfig).toEqual(null);
});
diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
index 952e48a71a9dc..66fcbd805f53e 100644
--- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
+++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
@@ -7,10 +7,8 @@
import type { FileLayer } from '@elastic/ems-client';
import { getEmsFileLayers } from '../util';
-import { emsWorldLayerId, emsRegionLayerId, emsUsaZipLayerId } from '../../common';
export interface SampleValuesConfig {
- emsLayerIds?: string[];
sampleValues?: Array;
sampleValuesColumnName?: string;
}
@@ -20,44 +18,16 @@ export interface EMSTermJoinConfig {
field: string;
}
-const wellKnownColumnNames = [
- {
- regex: /(geo\.){0,}country_iso_code$/i, // ECS postfix for country
- emsConfig: {
- layerId: emsWorldLayerId,
- field: 'iso2',
- },
- },
- {
- regex: /(geo\.){0,}region_iso_code$/i, // ECS postfixn for region
- emsConfig: {
- layerId: emsRegionLayerId,
- field: 'region_iso_code',
- },
- },
- {
- regex: /^country/i, // anything starting with country
- emsConfig: {
- layerId: emsWorldLayerId,
- field: 'name',
- },
- },
-];
-
-const wellKnownColumnFormats = [
- {
- regex: /(^\d{5}$)/i, // 5-digit zipcode
- emsConfig: {
- layerId: emsUsaZipLayerId,
- field: 'zip',
- },
- },
-];
-
interface UniqueMatch {
- config: { layerId: string; field: string };
+ config: EMSTermJoinConfig;
count: number;
}
+interface FileLayerFieldShim {
+ id: string;
+ values?: string[];
+ regex?: string;
+ alias?: string[];
+}
export async function suggestEMSTermJoinConfig(
sampleValuesConfig: SampleValuesConfig
@@ -65,20 +35,17 @@ export async function suggestEMSTermJoinConfig(
const matches: EMSTermJoinConfig[] = [];
if (sampleValuesConfig.sampleValuesColumnName) {
- matches.push(...suggestByName(sampleValuesConfig.sampleValuesColumnName));
+ const matchesBasedOnColumnName = await suggestByName(
+ sampleValuesConfig.sampleValuesColumnName,
+ sampleValuesConfig.sampleValues
+ );
+ matches.push(...matchesBasedOnColumnName);
}
if (sampleValuesConfig.sampleValues && sampleValuesConfig.sampleValues.length) {
- if (sampleValuesConfig.emsLayerIds && sampleValuesConfig.emsLayerIds.length) {
- matches.push(
- ...(await suggestByEMSLayerIds(
- sampleValuesConfig.emsLayerIds,
- sampleValuesConfig.sampleValues
- ))
- );
- } else {
- matches.push(...suggestByValues(sampleValuesConfig.sampleValues));
- }
+ // Only looks at id-values in main manifest
+ const matchesBasedOnIds = await suggestByIdValues(sampleValuesConfig.sampleValues);
+ matches.push(...matchesBasedOnIds);
}
const uniqMatches: UniqueMatch[] = matches.reduce((accum: UniqueMatch[], match) => {
@@ -105,92 +72,80 @@ export async function suggestEMSTermJoinConfig(
return uniqMatches.length ? uniqMatches[0].config : null;
}
-function suggestByName(columnName: string): EMSTermJoinConfig[] {
- const matches = wellKnownColumnNames.filter((wellknown) => {
- return columnName.match(wellknown.regex);
- });
-
- return matches.map((m) => {
- return m.emsConfig;
- });
-}
+async function suggestByName(
+ columnName: string,
+ sampleValues?: Array
+): Promise {
+ const fileLayers = await getEmsFileLayers();
-function suggestByValues(values: Array): EMSTermJoinConfig[] {
- const matches = wellKnownColumnFormats.filter((wellknown) => {
- for (let i = 0; i < values.length; i++) {
- const value = values[i].toString();
- if (!value.match(wellknown.regex)) {
- return false;
+ const matches: EMSTermJoinConfig[] = [];
+ fileLayers.forEach((fileLayer) => {
+ const emsFields: FileLayerFieldShim[] = fileLayer.getFields();
+ emsFields.forEach((emsField: FileLayerFieldShim) => {
+ if (!emsField.alias || !emsField.alias.length) {
+ return;
}
- }
- return true;
- });
- return matches.map((m) => {
- return m.emsConfig;
+ const emsConfig = {
+ layerId: fileLayer.getId(),
+ field: emsField.id,
+ };
+ emsField.alias.forEach((alias: string) => {
+ const regex = new RegExp(alias, 'i');
+ const nameMatchesAlias = !!columnName.match(regex);
+ // Check if this violates any known id-values.
+
+ let isMatch: boolean;
+ if (sampleValues) {
+ if (emsField.values && emsField.values.length) {
+ isMatch = nameMatchesAlias && allSamplesMatch(sampleValues, emsField.values);
+ } else {
+ // requires validation against sample-values but EMS provides no meta to do so.
+ isMatch = false;
+ }
+ } else {
+ isMatch = nameMatchesAlias;
+ }
+
+ if (isMatch) {
+ matches.push(emsConfig);
+ }
+ });
+ });
});
-}
-function existsInEMS(emsJson: any, emsFieldId: string, sampleValue: string): boolean {
- for (let i = 0; i < emsJson.features.length; i++) {
- const emsFieldValue = emsJson.features[i].properties[emsFieldId].toString();
- if (emsFieldValue.toString() === sampleValue) {
- return true;
- }
- }
- return false;
+ return matches;
}
-function matchesEmsField(emsJson: any, emsFieldId: string, sampleValues: Array) {
+function allSamplesMatch(sampleValues: Array, ids: string[]) {
for (let j = 0; j < sampleValues.length; j++) {
const sampleValue = sampleValues[j].toString();
- if (!existsInEMS(emsJson, emsFieldId, sampleValue)) {
+ if (!ids.includes(sampleValue)) {
return false;
}
}
return true;
}
-async function getMatchesForEMSLayer(
- emsLayerId: string,
+async function suggestByIdValues(
sampleValues: Array
): Promise {
+ const matches: EMSTermJoinConfig[] = [];
const fileLayers: FileLayer[] = await getEmsFileLayers();
- const emsFileLayer: FileLayer | undefined = fileLayers.find((fl: FileLayer) =>
- fl.hasId(emsLayerId)
- );
-
- if (!emsFileLayer) {
- return [];
- }
-
- const emsFields = emsFileLayer.getFields();
-
- try {
- const emsJson = await emsFileLayer.getGeoJson();
- const matches: EMSTermJoinConfig[] = [];
- for (let f = 0; f < emsFields.length; f++) {
- if (matchesEmsField(emsJson, emsFields[f].id, sampleValues)) {
- matches.push({
- layerId: emsLayerId,
- field: emsFields[f].id,
- });
+ fileLayers.forEach((fileLayer) => {
+ const emsFields: FileLayerFieldShim[] = fileLayer.getFields();
+ emsFields.forEach((emsField: FileLayerFieldShim) => {
+ if (!emsField.values || !emsField.values.length) {
+ return;
}
- }
- return matches;
- } catch (e) {
- return [];
- }
-}
-
-async function suggestByEMSLayerIds(
- emsLayerIds: string[],
- values: Array
-): Promise {
- const matches = [];
- for (const emsLayerId of emsLayerIds) {
- const layerIdMathes = await getMatchesForEMSLayer(emsLayerId, values);
- matches.push(...layerIdMathes);
- }
+ const emsConfig = {
+ layerId: fileLayer.getId(),
+ field: emsField.id,
+ };
+ if (allSamplesMatch(sampleValues, emsField.values)) {
+ matches.push(emsConfig);
+ }
+ });
+ });
return matches;
}
diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx
index 0dfff5a2c221e..92459ed28ab91 100644
--- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx
+++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx
@@ -44,7 +44,7 @@ import { getTopNavConfig } from '../top_nav_config';
import { MapQuery } from '../../../../common/descriptor_types';
import { goToSpecifiedPath } from '../../../render_app';
import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type';
-import { getExistingMapPath, APP_ID } from '../../../../common/constants';
+import { getFullPath, APP_ID } from '../../../../common/constants';
import {
getInitialQuery,
getInitialRefreshConfig,
@@ -356,7 +356,7 @@ export class MapApp extends React.Component {
const savedObjectId = this.props.savedMap.getSavedObjectId();
if (savedObjectId) {
getCoreChrome().recentlyAccessed.add(
- getExistingMapPath(savedObjectId),
+ getFullPath(savedObjectId),
this.props.savedMap.getTitle(),
savedObjectId
);
diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts
index f7e0012fdd9c2..45d3e0352acf6 100644
--- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts
+++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts
@@ -241,6 +241,10 @@ export class SavedMap {
return this._originatingApp;
}
+ public getOriginatingAppName(): string | undefined {
+ return this._originatingApp ? this.getAppNameFromId(this._originatingApp) : undefined;
+ }
+
public getAppNameFromId = (appId: string): string | undefined => {
return this._getStateTransfer().getAppNameFromId(appId);
};
diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx
index 7ac8c3070eb9d..79bc820d67b46 100644
--- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx
+++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx
@@ -151,9 +151,8 @@ export function getTopNavConfig({
const saveModalProps = {
onSave: async (
props: OnSaveProps & {
- returnToOrigin?: boolean;
dashboardId?: string | null;
- addToLibrary?: boolean;
+ addToLibrary: boolean;
}
) => {
try {
@@ -181,7 +180,7 @@ export function getTopNavConfig({
await savedMap.save({
...props,
newTags: selectedTags,
- saveByReference: Boolean(props.addToLibrary),
+ saveByReference: props.addToLibrary,
});
// showSaveModal wrapper requires onSave to return an object with an id to close the modal after successful save
return { id: 'id' };
@@ -204,8 +203,19 @@ export function getTopNavConfig({
saveModal = (
{
+ return saveModalProps.onSave({ ...props, addToLibrary: true });
+ }}
originatingApp={savedMap.getOriginatingApp()}
getAppNameFromId={savedMap.getAppNameFromId}
+ returnToOriginSwitchLabel={
+ savedMap.isByValue()
+ ? i18n.translate('xpack.maps.topNav.updatePanel', {
+ defaultMessage: 'Update panel on {originatingAppName}',
+ values: { originatingAppName: savedMap.getOriginatingAppName() },
+ })
+ : undefined
+ }
options={tagSelector}
/>
);
diff --git a/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts b/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts
index 268e5fa600b46..f05836dff2bd9 100644
--- a/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts
+++ b/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts
@@ -60,7 +60,9 @@ export function startAppStateSyncing(appStateManager: AppStateManager) {
stateContainer.set(initialAppState);
// set current url to whatever is in app state container
- kbnUrlStateStorage.set('_a', initialAppState);
+ kbnUrlStateStorage.set('_a', initialAppState, {
+ replace: true,
+ });
// finally start syncing state containers with url
startSyncingAppStateWithUrl();
diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts
index c753297932037..b8676559a4e2b 100644
--- a/x-pack/plugins/maps/server/plugin.ts
+++ b/x-pack/plugins/maps/server/plugin.ts
@@ -22,7 +22,7 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js';
// @ts-ignore
import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js';
import { registerMapsUsageCollector } from './maps_telemetry/collectors/register';
-import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../common/constants';
+import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getFullPath } from '../common/constants';
import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects';
import { MapsXPackConfig } from '../config';
// @ts-ignore
@@ -77,7 +77,7 @@ export class MapsPlugin implements Plugin {
home.sampleData.addAppLinksToSampleDataset('ecommerce', [
{
- path: getExistingMapPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'),
+ path: getFullPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'),
label: sampleDataLinkLabel,
icon: APP_ICON,
},
@@ -99,7 +99,7 @@ export class MapsPlugin implements Plugin {
home.sampleData.addAppLinksToSampleDataset('flights', [
{
- path: getExistingMapPath('5dd88580-1906-11e9-919b-ffe5949a18d2'),
+ path: getFullPath('5dd88580-1906-11e9-919b-ffe5949a18d2'),
label: sampleDataLinkLabel,
icon: APP_ICON,
},
@@ -120,7 +120,7 @@ export class MapsPlugin implements Plugin {
home.sampleData.addSavedObjectsToSampleDataset('logs', getWebLogsSavedObjects());
home.sampleData.addAppLinksToSampleDataset('logs', [
{
- path: getExistingMapPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'),
+ path: getFullPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'),
label: sampleDataLinkLabel,
icon: APP_ICON,
},
diff --git a/x-pack/plugins/maps/server/saved_objects/map.ts b/x-pack/plugins/maps/server/saved_objects/map.ts
index 78f70e27b2b7b..24effd651a31b 100644
--- a/x-pack/plugins/maps/server/saved_objects/map.ts
+++ b/x-pack/plugins/maps/server/saved_objects/map.ts
@@ -6,7 +6,7 @@
*/
import { SavedObjectsType } from 'src/core/server';
-import { APP_ICON, getExistingMapPath } from '../../common/constants';
+import { APP_ICON, getFullPath } from '../../common/constants';
// @ts-ignore
import { savedObjectMigrations } from './saved_object_migrations';
@@ -34,7 +34,7 @@ export const mapSavedObjects: SavedObjectsType = {
},
getInAppUrl(obj) {
return {
- path: getExistingMapPath(obj.id),
+ path: getFullPath(obj.id),
uiCapabilitiesPath: 'maps.show',
};
},
diff --git a/x-pack/plugins/maps/server/tutorials/ems/index.ts b/x-pack/plugins/maps/server/tutorials/ems/index.ts
index 410c833b8ac77..3c63850f87291 100644
--- a/x-pack/plugins/maps/server/tutorials/ems/index.ts
+++ b/x-pack/plugins/maps/server/tutorials/ems/index.ts
@@ -16,6 +16,48 @@ export function emsBoundariesSpecProvider({
emsLandingPageUrl: string;
prependBasePath: (path: string) => string;
}) {
+ const instructions = {
+ instructionSets: [
+ {
+ instructionVariants: [
+ {
+ id: 'EMS',
+ instructions: [
+ {
+ title: i18n.translate('xpack.maps.tutorials.ems.downloadStepTitle', {
+ defaultMessage: 'Download Elastic Maps Service boundaries',
+ }),
+ textPre: i18n.translate('xpack.maps.tutorials.ems.downloadStepText', {
+ defaultMessage:
+ '1. Navigate to Elastic Maps Service [landing page]({emsLandingPageUrl}/).\n\
+2. In the left sidebar, select an administrative boundary.\n\
+3. Click `Download GeoJSON` button.',
+ values: {
+ emsLandingPageUrl,
+ },
+ }),
+ },
+ {
+ title: i18n.translate('xpack.maps.tutorials.ems.uploadStepTitle', {
+ defaultMessage: 'Index Elastic Maps Service boundaries',
+ }),
+ textPre: i18n.translate('xpack.maps.tutorials.ems.uploadStepText', {
+ defaultMessage:
+ '1. Open [Maps]({newMapUrl}).\n\
+2. Click `Add layer`, then select `Upload GeoJSON`.\n\
+3. Upload the GeoJSON file and click `Import file`.',
+ values: {
+ newMapUrl: prependBasePath(getNewMapPath()),
+ },
+ }),
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
return () => ({
id: 'emsBoundaries',
name: i18n.translate('xpack.maps.tutorials.ems.nameTitle', {
@@ -34,46 +76,7 @@ Indexing EMS administrative boundaries in Elasticsearch allows for search on bou
euiIconType: 'emsApp',
completionTimeMinutes: 1,
previewImagePath: '/plugins/maps/assets/boundaries_screenshot.png',
- onPrem: {
- instructionSets: [
- {
- instructionVariants: [
- {
- id: 'EMS',
- instructions: [
- {
- title: i18n.translate('xpack.maps.tutorials.ems.downloadStepTitle', {
- defaultMessage: 'Download Elastic Maps Service boundaries',
- }),
- textPre: i18n.translate('xpack.maps.tutorials.ems.downloadStepText', {
- defaultMessage:
- '1. Navigate to Elastic Maps Service [landing page]({emsLandingPageUrl}).\n\
-2. In the left sidebar, select an administrative boundary.\n\
-3. Click `Download GeoJSON` button.',
- values: {
- emsLandingPageUrl,
- },
- }),
- },
- {
- title: i18n.translate('xpack.maps.tutorials.ems.uploadStepTitle', {
- defaultMessage: 'Index Elastic Maps Service boundaries',
- }),
- textPre: i18n.translate('xpack.maps.tutorials.ems.uploadStepText', {
- defaultMessage:
- '1. Open [Maps]({newMapUrl}).\n\
-2. Click `Add layer`, then select `Upload GeoJSON`.\n\
-3. Upload the GeoJSON file and click `Import file`.',
- values: {
- newMapUrl: prependBasePath(getNewMapPath()),
- },
- }),
- },
- ],
- },
- ],
- },
- ],
- },
+ onPrem: instructions,
+ elasticCloud: instructions,
});
}
diff --git a/x-pack/plugins/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts
index fa40cefcaed48..74d3286438588 100644
--- a/x-pack/plugins/ml/common/types/results.ts
+++ b/x-pack/plugins/ml/common/types/results.ts
@@ -6,6 +6,7 @@
*/
import { estypes } from '@elastic/elasticsearch';
+import { LineAnnotationDatum, RectAnnotationDatum } from '@elastic/charts';
export interface GetStoppedPartitionResult {
jobs: string[] | Record;
@@ -13,6 +14,9 @@ export interface GetStoppedPartitionResult {
export interface GetDatafeedResultsChartDataResult {
bucketResults: number[][];
datafeedResults: number[][];
+ annotationResultsRect: RectAnnotationDatum[];
+ annotationResultsLine: LineAnnotationDatum[];
+ modelSnapshotResultsLine: LineAnnotationDatum[];
}
export interface DatafeedResultsChartDataParams {
diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json
index e3bcf307e6f00..7b3f457106033 100644
--- a/x-pack/plugins/ml/kibana.json
+++ b/x-pack/plugins/ml/kibana.json
@@ -27,7 +27,6 @@
"management",
"licenseManagement",
"maps",
- "lens",
"usageCollection"
],
"server": true,
diff --git a/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts b/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts
index 0907cce832bf8..f16ba27524670 100644
--- a/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts
+++ b/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts
@@ -9,7 +9,6 @@ import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/publi
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { kibanaLegacyPluginMock } from '../../../../../src/plugins/kibana_legacy/public/mocks';
import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks';
-import { lensPluginMock } from '../../../lens/public/mocks';
import { triggersActionsUiMock } from '../../../triggers_actions_ui/public/mocks';
export const createMlStartDepsMock = () => ({
@@ -22,7 +21,6 @@ export const createMlStartDepsMock = () => ({
spaces: jest.fn(),
embeddable: embeddablePluginMock.createStartContract(),
maps: jest.fn(),
- lens: lensPluginMock.createStartContract(),
triggersActionsUi: triggersActionsUiMock.createStart(),
dataVisualizer: jest.fn(),
});
diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx
index 8be513f372e56..222d23acb40a7 100644
--- a/x-pack/plugins/ml/public/application/app.tsx
+++ b/x-pack/plugins/ml/public/application/app.tsx
@@ -77,7 +77,6 @@ const App: FC = ({ coreStart, deps, appMountParams }) => {
data: deps.data,
security: deps.security,
licenseManagement: deps.licenseManagement,
- lens: deps.lens,
storage: localStorage,
embeddable: deps.embeddable,
maps: deps.maps,
diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
index afed7e79ff757..b68e64a5d9f6a 100644
--- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
+++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
@@ -494,13 +494,13 @@ class AnnotationsTableUI extends Component {
render: (annotation) => {
const viewDataFeedText = (
);
const viewDataFeedTooltipAriaLabelText = i18n.translate(
- 'xpack.ml.annotationsTable.viewDatafeedTooltipAriaLabel',
- { defaultMessage: 'View datafeed' }
+ 'xpack.ml.annotationsTable.datafeedChartTooltipAriaLabel',
+ { defaultMessage: 'Datafeed chart' }
);
return (
) : null}
diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
index 841f0d03fa21c..1ade617fa60a5 100644
--- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
+++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
@@ -19,7 +19,6 @@ import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/p
import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public';
import type { MapsStartApi } from '../../../../../maps/public';
import type { DataVisualizerPluginStart } from '../../../../../data_visualizer/public';
-import type { LensPublicStart } from '../../../../../lens/public';
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
interface StartPlugins {
@@ -29,7 +28,6 @@ interface StartPlugins {
share: SharePluginStart;
embeddable: EmbeddableStart;
maps?: MapsStartApi;
- lens?: LensPublicStart;
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
dataVisualizer?: DataVisualizerPluginStart;
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx
index d24ec2126aee8..766f1bda64d5e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx
@@ -22,7 +22,6 @@ import {
EuiFlyoutHeader,
EuiForm,
EuiFormRow,
- EuiOverlayMask,
EuiSelect,
EuiTitle,
} from '@elastic/eui';
@@ -129,188 +128,180 @@ export const EditActionFlyout: FC> = ({ closeFlyout, item }
};
return (
-
-
-
-
-
- {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutTitle', {
- defaultMessage: 'Edit {jobId}',
- values: {
- jobId,
- },
- })}
-
-
-
-
-
-
+
+
+
+ {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutTitle', {
+ defaultMessage: 'Edit {jobId}',
+ values: {
+ jobId,
+ },
+ })}
+
+
+
+
+
+
+
- ) =>
- setAllowLazyStart(e.target.value)
- }
- />
-
- ) =>
+ setAllowLazyStart(e.target.value)
+ }
+ />
+
+
+ setDescription(e.target.value)}
+ aria-label={i18n.translate(
+ 'xpack.ml.dataframe.analyticsList.editFlyout.descriptionAriaLabel',
+ {
+ defaultMessage: 'Update the job description.',
}
)}
- >
- setDescription(e.target.value)}
- aria-label={i18n.translate(
- 'xpack.ml.dataframe.analyticsList.editFlyout.descriptionAriaLabel',
- {
- defaultMessage: 'Update the job description.',
- }
- )}
- />
-
-
+
+
+ setModelMemoryLimit(e.target.value)}
+ aria-label={i18n.translate(
+ 'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitAriaLabel',
{
- defaultMessage: 'Model memory limit',
+ defaultMessage: 'Update the model memory limit.',
}
)}
- isInvalid={mmlValidationError !== undefined}
- error={mmlValidationError}
- >
- setModelMemoryLimit(e.target.value)}
- aria-label={i18n.translate(
- 'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitAriaLabel',
- {
- defaultMessage: 'Update the model memory limit.',
- }
- )}
- />
-
-
+
+
+
+ setMaxNumThreads(e.target.value === '' ? undefined : +e.target.value)
}
+ step={1}
+ min={1}
+ readOnly={state !== DATA_FRAME_TASK_STATE.STOPPED}
+ value={maxNumThreads}
+ />
+
+
+
+
+
+
+
+ {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutCancelButtonText', {
+ defaultMessage: 'Cancel',
+ })}
+
+
+
+
-
- setMaxNumThreads(e.target.value === '' ? undefined : +e.target.value)
- }
- step={1}
- min={1}
- readOnly={state !== DATA_FRAME_TASK_STATE.STOPPED}
- value={maxNumThreads}
- />
-
-
-
-
-
-
-
- {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutCancelButtonText', {
- defaultMessage: 'Cancel',
- })}
-
-
-
-
- {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText', {
- defaultMessage: 'Update',
- })}
-
-
-
-
-
-
+ {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText', {
+ defaultMessage: 'Update',
+ })}
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx
index 88ffaa0da7fdc..93be45bbdaf97 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx
@@ -114,10 +114,7 @@ export const ExpandedRow: FC = ({ item }) => {
}
const {
- services: {
- share,
- application: { navigateToUrl },
- },
+ services: { share },
} = useMlKibana();
const tabs = [
@@ -402,17 +399,16 @@ export const ExpandedRow: FC = ({ item }) => {
{
- const ingestPipelinesAppUrlGenerator = share.urlGenerators.getUrlGenerator(
- 'INGEST_PIPELINES_APP_URL_GENERATOR'
- );
- await navigateToUrl(
- await ingestPipelinesAppUrlGenerator.createUrl({
- page: 'pipeline_edit',
- pipelineId: pipelineName,
- absolute: true,
- })
+ onClick={() => {
+ const locator = share.url.locators.get(
+ 'INGEST_PIPELINES_APP_LOCATOR'
);
+ if (!locator) return;
+ locator.navigate({
+ page: 'pipeline_edit',
+ pipelineId: pipelineName,
+ absolute: true,
+ });
}}
>
= ({ anomalies, jobIds }) => {
}
const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({
- emsLayerIds: COMMON_EMS_LAYER_IDS,
sampleValues: Array.from(entityValues),
sampleValuesColumnName: entityName || '',
});
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts
index 71f3795518bc9..b3b9487523196 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts
@@ -15,7 +15,7 @@ export const CHART_DIRECTION = {
export type ChartDirectionType = typeof CHART_DIRECTION[keyof typeof CHART_DIRECTION];
// [width, height]
-export const CHART_SIZE: ChartSizeArray = ['100%', 300];
+export const CHART_SIZE: ChartSizeArray = ['100%', 380];
export const TAB_IDS = {
CHART: 'chart',
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx
index cf547a49cac4c..2dece82e6f5c7 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx
@@ -11,25 +11,35 @@ import { i18n } from '@kbn/i18n';
import moment from 'moment';
import {
EuiButtonEmpty,
+ EuiCheckbox,
EuiDatePicker,
EuiFlexGroup,
EuiFlexItem,
+ EuiIcon,
+ EuiIconTip,
EuiLoadingChart,
EuiModal,
EuiModalHeader,
EuiModalBody,
- EuiSelect,
EuiSpacer,
EuiTabs,
EuiTab,
+ EuiText,
+ EuiTitle,
EuiToolTip,
+ htmlIdGenerator,
} from '@elastic/eui';
import {
+ AnnotationDomainType,
Axis,
Chart,
CurveType,
+ LineAnnotation,
LineSeries,
+ LineAnnotationDatum,
Position,
+ RectAnnotation,
+ RectAnnotationDatum,
ScaleType,
Settings,
timeFormatter,
@@ -42,7 +52,6 @@ import { useMlApiContext } from '../../../../contexts/kibana';
import { useCurrentEuiTheme } from '../../../../components/color_range_legend';
import { JobMessagesPane } from '../job_details/job_messages_pane';
import { EditQueryDelay } from './edit_query_delay';
-import { getIntervalOptions } from './get_interval_options';
import {
CHART_DIRECTION,
ChartDirectionType,
@@ -53,12 +62,18 @@ import {
} from './constants';
import { loadFullJob } from '../utils';
-const dateFormatter = timeFormatter('MM-DD HH:mm');
+const dateFormatter = timeFormatter('MM-DD HH:mm:ss');
+const MAX_CHART_POINTS = 480;
interface DatafeedModalProps {
jobId: string;
end: number;
- onClose: (deletionApproved?: boolean) => void;
+ onClose: () => void;
+}
+
+function setLineAnnotationHeader(lineDatum: LineAnnotationDatum) {
+ lineDatum.header = dateFormatter(lineDatum.dataValue);
+ return lineDatum;
}
export const DatafeedModal: FC = ({ jobId, end, onClose }) => {
@@ -68,11 +83,17 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
isInitialized: boolean;
}>({ datafeedConfig: undefined, bucketSpan: undefined, isInitialized: false });
const [endDate, setEndDate] = useState(moment(end));
- const [interval, setInterval] = useState();
const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.CHART);
const [isLoadingChartData, setIsLoadingChartData] = useState(false);
const [bucketData, setBucketData] = useState([]);
+ const [annotationData, setAnnotationData] = useState<{
+ rect: RectAnnotationDatum[];
+ line: LineAnnotationDatum[];
+ }>({ rect: [], line: [] });
+ const [modelSnapshotData, setModelSnapshotData] = useState([]);
const [sourceData, setSourceData] = useState([]);
+ const [showAnnotations, setShowAnnotations] = useState(true);
+ const [showModelSnapshots, setShowModelSnapshots] = useState(true);
const {
results: { getDatafeedResultChartData },
@@ -102,25 +123,30 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
const handleChange = (date: moment.Moment) => setEndDate(date);
const handleEndDateChange = (direction: ChartDirectionType) => {
- if (interval === undefined) return;
+ if (data.bucketSpan === undefined) return;
const newEndDate = endDate.clone();
- const [count, type] = interval.split(' ');
+ const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!;
+ const unit = unitMatch[0];
+ const count = Number(data.bucketSpan.replace(/[^0-9]/g, ''));
if (direction === CHART_DIRECTION.FORWARD) {
- newEndDate.add(Number(count), type);
+ newEndDate.add(MAX_CHART_POINTS * count, unit);
} else {
- newEndDate.subtract(Number(count), type);
+ newEndDate.subtract(MAX_CHART_POINTS * count, unit);
}
setEndDate(newEndDate);
};
const getChartData = useCallback(async () => {
- if (interval === undefined) return;
+ if (data.bucketSpan === undefined) return;
const endTimestamp = moment(endDate).valueOf();
- const [count, type] = interval.split(' ');
- const startMoment = endDate.clone().subtract(Number(count), type);
+ const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!;
+ const unit = unitMatch[0];
+ const count = Number(data.bucketSpan.replace(/[^0-9]/g, ''));
+ // STARTTIME = ENDTIME - (BucketSpan * MAX_CHART_POINTS)
+ const startMoment = endDate.clone().subtract(MAX_CHART_POINTS * count, unit);
const startTimestamp = moment(startMoment).valueOf();
try {
@@ -128,6 +154,11 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
setSourceData(chartData.datafeedResults);
setBucketData(chartData.bucketResults);
+ setAnnotationData({
+ rect: chartData.annotationResultsRect,
+ line: chartData.annotationResultsLine.map(setLineAnnotationHeader),
+ });
+ setModelSnapshotData(chartData.modelSnapshotResultsLine.map(setLineAnnotationHeader));
} catch (error) {
const title = i18n.translate('xpack.ml.jobsList.datafeedModal.errorToastTitle', {
defaultMessage: 'Error fetching data',
@@ -135,7 +166,7 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
displayErrorToast(error, title);
}
setIsLoadingChartData(false);
- }, [endDate, interval]);
+ }, [endDate, data.bucketSpan]);
const getJobData = async () => {
try {
@@ -145,11 +176,6 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
bucketSpan: job.analysis_config.bucket_span,
isInitialized: true,
});
- const intervalOptions = getIntervalOptions(job.analysis_config.bucket_span);
- const initialInterval = intervalOptions.length
- ? intervalOptions[intervalOptions.length - 1]
- : undefined;
- setInterval(initialInterval?.value || '72 hours');
} catch (error) {
displayErrorToast(error);
}
@@ -161,20 +187,17 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
useEffect(
function loadChartData() {
- if (interval !== undefined) {
+ if (data.bucketSpan !== undefined) {
setIsLoadingChartData(true);
getChartData();
}
},
- [endDate, interval]
+ [endDate, data.bucketSpan]
);
const { datafeedConfig, bucketSpan, isInitialized } = data;
-
- const intervalOptions = useMemo(() => {
- if (bucketSpan === undefined) return [];
- return getIntervalOptions(bucketSpan);
- }, [bucketSpan]);
+ const checkboxIdAnnotation = useMemo(() => htmlIdGenerator()(), []);
+ const checkboxIdModelSnapshot = useMemo(() => htmlIdGenerator()(), []);
return (
= ({ jobId, end, onClose }) =
-
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
= ({ jobId, end, onClose }) =
-
- setInterval(e.target.value)}
- aria-label={i18n.translate(
- 'xpack.ml.jobsList.datafeedModal.intervalSelection',
- {
- defaultMessage: 'Datafeed modal chart interval selection',
- }
- )}
- />
-
= ({ jobId, end, onClose }) =
isEnabled={datafeedConfig.state === DATAFEED_STATE.STOPPED}
/>
+
+
+
+
+
+
+ }
+ checked={showAnnotations}
+ onChange={() => setShowAnnotations(!showAnnotations)}
+ />
+
+
+
+
+
+ }
+ checked={showModelSnapshots}
+ onChange={() => setShowModelSnapshots(!showModelSnapshots)}
+ />
+
+
+
@@ -298,7 +362,65 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
})}
position={Position.Left}
/>
+ {showModelSnapshots ? (
+ }
+ markerPosition={Position.Top}
+ style={{
+ line: {
+ strokeWidth: 3,
+ stroke: euiTheme.euiColorVis1,
+ opacity: 0.5,
+ },
+ }}
+ />
+ ) : null}
+ {showAnnotations ? (
+ <>
+ }
+ markerPosition={Position.Top}
+ style={{
+ line: {
+ strokeWidth: 3,
+ stroke: euiTheme.euiColorDangerText,
+ opacity: 0.5,
+ },
+ }}
+ />
+
+ >
+ ) : null}
= ({ jobId, end, onClose }) =
curve={CurveType.LINEAR}
/>
{
- const unitMatch = bucketSpan.match(/[d | h| m | s]/g)!;
- const unit = unitMatch[0];
- const count = Number(bucketSpan.replace(/[^0-9]/g, ''));
-
- const intervalOptions = [];
-
- if (['s', 'ms', 'micros', 'nanos'].includes(unit)) {
- intervalOptions.push(
- {
- value: '1 hour',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.1hourOption', {
- defaultMessage: '{count} hour',
- values: { count: 1 },
- }),
- },
- {
- value: '2 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.2hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 2 },
- }),
- }
- );
- }
-
- if ((unit === 'm' && count <= 4) || unit === 'h') {
- intervalOptions.push(
- {
- value: '3 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.3hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 3 },
- }),
- },
- {
- value: '8 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.8hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 8 },
- }),
- },
- {
- value: '12 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.12hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 12 },
- }),
- },
- {
- value: '24 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.24hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 24 },
- }),
- }
- );
- }
-
- if ((unit === 'm' && count >= 5 && count <= 15) || unit === 'h') {
- intervalOptions.push(
- {
- value: '48 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.48hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 48 },
- }),
- },
- {
- value: '72 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.72hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 72 },
- }),
- }
- );
- }
-
- if ((unit === 'm' && count >= 10 && count <= 15) || unit === 'h' || unit === 'd') {
- intervalOptions.push(
- {
- value: '5 days',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.5daysOption', {
- defaultMessage: '{count} days',
- values: { count: 5 },
- }),
- },
- {
- value: '7 days',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.7daysOption', {
- defaultMessage: '{count} days',
- values: { count: 7 },
- }),
- }
- );
- }
-
- if (unit === 'h' || unit === 'd') {
- intervalOptions.push({
- value: '14 days',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.14DaysOption', {
- defaultMessage: '{count} days',
- values: { count: 14 },
- }),
- });
- }
-
- return intervalOptions;
-};
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
index b514c8433daf4..d3856e6afa398 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
@@ -7,26 +7,29 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
-
-import { EuiTabbedContent, EuiLoadingSpinner } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { EuiButtonIcon, EuiTabbedContent, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
import { extractJobDetails } from './extract_job_details';
import { JsonPane } from './json_tab';
import { DatafeedPreviewPane } from './datafeed_preview_tab';
import { AnnotationsTable } from '../../../../components/annotations/annotations_table';
+import { DatafeedModal } from '../datafeed_modal';
import { AnnotationFlyout } from '../../../../components/annotations/annotation_flyout';
import { ModelSnapshotTable } from '../../../../components/model_snapshots';
import { ForecastsTable } from './forecasts_table';
import { JobDetailsPane } from './job_details_pane';
import { JobMessagesPane } from './job_messages_pane';
-import { i18n } from '@kbn/i18n';
import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
export class JobDetailsUI extends Component {
constructor(props) {
super(props);
- this.state = {};
+ this.state = {
+ datafeedModalVisible: false,
+ };
if (this.props.addYourself) {
this.props.addYourself(props.jobId, (j) => this.updateJob(j));
}
@@ -77,6 +80,30 @@ export class JobDetailsUI extends Component {
alertRules,
} = extractJobDetails(job, basePath, refreshJobList);
+ datafeed.titleAction = (
+
+ }
+ >
+
+ this.setState({
+ datafeedModalVisible: true,
+ })
+ }
+ />
+
+ );
+
const tabs = [
{
id: 'job-settings',
@@ -105,6 +132,32 @@ export class JobDetailsUI extends Component {
/>
),
},
+ {
+ id: 'datafeed',
+ 'data-test-subj': 'mlJobListTab-datafeed',
+ name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', {
+ defaultMessage: 'Datafeed',
+ }),
+ content: (
+ <>
+
+ {this.props.jobId && this.state.datafeedModalVisible ? (
+ {
+ this.setState({
+ datafeedModalVisible: false,
+ });
+ }}
+ end={job.data_counts.latest_bucket_timestamp}
+ jobId={this.props.jobId}
+ />
+ ) : null}
+ >
+ ),
+ },
{
id: 'counts',
'data-test-subj': 'mlJobListTab-counts',
@@ -137,21 +190,6 @@ export class JobDetailsUI extends Component {
];
if (showFullDetails && datafeed.items.length) {
- // Datafeed should be at index 2 in tabs array for full details
- tabs.splice(2, 0, {
- id: 'datafeed',
- 'data-test-subj': 'mlJobListTab-datafeed',
- name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', {
- defaultMessage: 'Datafeed',
- }),
- content: (
-
- ),
- });
-
tabs.push(
{
id: 'datafeed-preview',
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
index 49d9bcde49052..4046f4d5d8071 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
@@ -9,6 +9,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import {
+ EuiFlexGroup,
+ EuiFlexItem,
EuiTitle,
EuiTable,
EuiTableBody,
@@ -42,9 +44,14 @@ function Section({ section }) {
return (
-
- {section.title}
-
+
+
+
+ {section.title}
+
+
+ {section.titleAction}
+
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
index 19ba5aa304bf0..25ef36782207f 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
@@ -6,7 +6,10 @@
*/
// Service for obtaining data for the ML Results dashboards.
-import { GetStoppedPartitionResult } from '../../../../common/types/results';
+import {
+ GetStoppedPartitionResult,
+ GetDatafeedResultsChartDataResult,
+} from '../../../../common/types/results';
import { HttpService } from '../http_service';
import { basePath } from './index';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
@@ -148,7 +151,7 @@ export const resultsApiProvider = (httpService: HttpService) => ({
start,
end,
});
- return httpService.http({
+ return httpService.http({
path: `${basePath()}/results/datafeed_results_chart`,
method: 'POST',
body,
diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts
index e3a4a8348ebc1..917619a67fea9 100644
--- a/x-pack/plugins/ml/public/plugin.ts
+++ b/x-pack/plugins/ml/public/plugin.ts
@@ -44,7 +44,6 @@ import { registerFeature } from './register_feature';
// Not importing from `ml_url_generator/index` here to avoid importing unnecessary code
import { registerUrlGenerator } from './ml_url_generator/ml_url_generator';
import type { MapsStartApi } from '../../maps/public';
-import { LensPublicStart } from '../../lens/public';
import {
TriggersAndActionsUIPublicPluginSetup,
TriggersAndActionsUIPublicPluginStart,
@@ -62,7 +61,6 @@ export interface MlStartDependencies {
spaces?: SpacesPluginStart;
embeddable: EmbeddableStart;
maps?: MapsStartApi;
- lens?: LensPublicStart;
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
dataVisualizer: DataVisualizerPluginStart;
}
@@ -119,7 +117,6 @@ export class MlPlugin implements Plugin {
embeddable: { ...pluginsSetup.embeddable, ...pluginsStart.embeddable },
maps: pluginsStart.maps,
uiActions: pluginsStart.uiActions,
- lens: pluginsStart.lens,
kibanaVersion,
triggersActionsUi: pluginsStart.triggersActionsUi,
dataVisualizer: pluginsStart.dataVisualizer,
diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts
index 9413ee00184d2..81ee394b99704 100644
--- a/x-pack/plugins/ml/server/models/results_service/results_service.ts
+++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts
@@ -27,6 +27,7 @@ import {
import { MlJobsResponse } from '../../../common/types/job_service';
import type { MlClient } from '../../lib/ml_client';
import { datafeedsProvider } from '../job_service/datafeeds';
+import { annotationServiceProvider } from '../annotation_service';
// Service for carrying out Elasticsearch queries to obtain data for the
// ML Results dashboards.
@@ -620,13 +621,19 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
const finalResults: GetDatafeedResultsChartDataResult = {
bucketResults: [],
datafeedResults: [],
+ annotationResultsRect: [],
+ annotationResultsLine: [],
+ modelSnapshotResultsLine: [],
};
const { getDatafeedByJobId } = datafeedsProvider(client!, mlClient);
- const datafeedConfig = await getDatafeedByJobId(jobId);
- const { body: jobsResponse } = await mlClient.getJobs({ job_id: jobId });
- if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) {
+ const [datafeedConfig, { body: jobsResponse }] = await Promise.all([
+ getDatafeedByJobId(jobId),
+ mlClient.getJobs({ job_id: jobId }),
+ ]);
+
+ if (jobsResponse && (jobsResponse.count === 0 || jobsResponse.jobs === undefined)) {
throw Boom.notFound(`Job with the id "${jobId}" not found`);
}
@@ -696,10 +703,25 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
]) || [];
}
- const bucketResp = await mlClient.getBuckets({
- job_id: jobId,
- body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
- });
+ const { getAnnotations } = annotationServiceProvider(client!);
+
+ const [bucketResp, annotationResp, { body: modelSnapshotsResp }] = await Promise.all([
+ mlClient.getBuckets({
+ job_id: jobId,
+ body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
+ }),
+ getAnnotations({
+ jobIds: [jobId],
+ earliestMs: start,
+ latestMs: end,
+ maxAnnotations: 1000,
+ }),
+ mlClient.getModelSnapshots({
+ job_id: jobId,
+ start: String(start),
+ end: String(end),
+ }),
+ ]);
const bucketResults = bucketResp?.body?.buckets ?? [];
bucketResults.forEach((dataForTime) => {
@@ -708,6 +730,36 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
finalResults.bucketResults.push([timestamp, eventCount]);
});
+ const annotationResults = annotationResp.annotations[jobId] || [];
+ annotationResults.forEach((annotation) => {
+ const timestamp = Number(annotation?.timestamp);
+ const endTimestamp = Number(annotation?.end_timestamp);
+ if (timestamp === endTimestamp) {
+ finalResults.annotationResultsLine.push({
+ dataValue: timestamp,
+ details: annotation.annotation,
+ });
+ } else {
+ finalResults.annotationResultsRect.push({
+ coordinates: {
+ x0: timestamp,
+ x1: endTimestamp,
+ },
+ details: annotation.annotation,
+ });
+ }
+ });
+
+ const modelSnapshots = modelSnapshotsResp?.model_snapshots ?? [];
+ modelSnapshots.forEach((modelSnapshot) => {
+ const timestamp = Number(modelSnapshot?.timestamp);
+
+ finalResults.modelSnapshotResultsLine.push({
+ dataValue: timestamp,
+ details: modelSnapshot.description,
+ });
+ });
+
return finalResults;
}
diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json
index 221718d423383..8e859c35e3f85 100644
--- a/x-pack/plugins/ml/tsconfig.json
+++ b/x-pack/plugins/ml/tsconfig.json
@@ -16,7 +16,7 @@
"../../../typings/**/*",
// have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636
"public/**/*.json",
- "server/**/*.json",
+ "server/**/*.json"
],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
@@ -28,7 +28,6 @@
{ "path": "../license_management/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
{ "path": "../maps/tsconfig.json" },
- { "path": "../lens/tsconfig.json" },
{ "path": "../security/tsconfig.json" },
{ "path": "../spaces/tsconfig.json" },
{ "path": "../alerting/tsconfig.json" },
diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx
index 8b4075ba67cdc..44af8b3327975 100644
--- a/x-pack/plugins/monitoring/public/alerts/badge.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx
@@ -19,13 +19,18 @@ import { getAlertPanelsByCategory } from './lib/get_alert_panels_by_category';
import { getAlertPanelsByNode } from './lib/get_alert_panels_by_node';
export const numberOfAlertsLabel = (count: number) => `${count} alert${count > 1 ? 's' : ''}`;
+export const numberOfRulesLabel = (count: number) => `${count} rule${count > 1 ? 's' : ''}`;
const MAX_TO_SHOW_BY_CATEGORY = 8;
-const PANEL_TITLE = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
+const PANEL_TITLE_ALERTS = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
defaultMessage: 'Alerts',
});
+const PANEL_TITLE_RULES = i18n.translate('xpack.monitoring.rules.badge.panelTitle', {
+ defaultMessage: 'Rules',
+});
+
const GROUP_BY_NODE = i18n.translate('xpack.monitoring.alerts.badge.groupByNode', {
defaultMessage: 'Group by node',
});
@@ -54,6 +59,7 @@ export const AlertsBadge: React.FC = (props: Props) => {
const [showByNode, setShowByNode] = React.useState(
!inSetupMode && alertCount > MAX_TO_SHOW_BY_CATEGORY
);
+ const PANEL_TITLE = inSetupMode ? PANEL_TITLE_RULES : PANEL_TITLE_ALERTS;
React.useEffect(() => {
if (inSetupMode && showByNode) {
@@ -93,10 +99,12 @@ export const AlertsBadge: React.FC = (props: Props) => {
setShowPopover(true)}
>
- {numberOfAlertsLabel(alertCount)}
+ {inSetupMode ? numberOfRulesLabel(alertCount) : numberOfAlertsLabel(alertCount)}
);
diff --git a/x-pack/plugins/monitoring/public/alerts/configuration.tsx b/x-pack/plugins/monitoring/public/alerts/configuration.tsx
index 5416095671d71..7825fe8e20617 100644
--- a/x-pack/plugins/monitoring/public/alerts/configuration.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/configuration.tsx
@@ -32,7 +32,7 @@ export const AlertConfiguration: React.FC = (props: Props) => {
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.disableAlert.errorTitle', {
- defaultMessage: `Unable to disable alert`,
+ defaultMessage: `Unable to disable rule`,
}),
text: err.message,
});
@@ -46,7 +46,7 @@ export const AlertConfiguration: React.FC = (props: Props) => {
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.enableAlert.errorTitle', {
- defaultMessage: `Unable to enable alert`,
+ defaultMessage: `Unable to enable rule`,
}),
text: err.message,
});
@@ -60,7 +60,7 @@ export const AlertConfiguration: React.FC = (props: Props) => {
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.muteAlert.errorTitle', {
- defaultMessage: `Unable to mute alert`,
+ defaultMessage: `Unable to mute rule`,
}),
text: err.message,
});
@@ -74,7 +74,7 @@ export const AlertConfiguration: React.FC = (props: Props) => {
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.ummuteAlert.errorTitle', {
- defaultMessage: `Unable to unmute alert`,
+ defaultMessage: `Unable to unmute rule`,
}),
text: err.message,
});
@@ -112,7 +112,7 @@ export const AlertConfiguration: React.FC = (props: Props) => {
}}
>
{i18n.translate('xpack.monitoring.alerts.panel.editAlert', {
- defaultMessage: `Edit alert`,
+ defaultMessage: `Edit rule`,
})}
diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap
index 5018bad317708..e3fa9da6639b3 100644
--- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap
@@ -22,33 +22,37 @@ Array [
-
- There were some errors encountered in trying to check Elasticsearch settings. You need administrator rights to check the settings and, if needed, to enable the monitoring collection setting.
-
-
-
+ There were some errors encountered in trying to check Elasticsearch settings. You need administrator rights to check the settings and, if needed, to enable the monitoring collection setting.
+
+
- 403 Forbidden
-
-
- no access for you
-
-
- 500 Internal Server Error
-
-
- An internal server error occurred
-
-
+
+ 403 Forbidden
+
+
+ no access for you
+
+
+ 500 Internal Server Error
+
+
+ An internal server error occurred
+
+
+
,
]
diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
index fe277062bc95a..34a4c049dddcc 100644
--- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
@@ -9,7 +9,7 @@ exports[`NoData should show a default message if reason is unknown 1`] = `
>
No monitoring data found.
-
@@ -87,7 +87,7 @@ exports[`NoData should show a default message if reason is unknown 1`] = `
-
+
`;
@@ -100,7 +100,7 @@ exports[`NoData should show text next to the spinner while checking a setting 1`
>
No monitoring data found.
-
@@ -178,6 +178,6 @@ exports[`NoData should show text next to the spinner while checking a setting 1`
-
+
`;
diff --git a/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap
index 7f38a92beae8f..7b04e6410d996 100644
--- a/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap
@@ -5,7 +5,7 @@ exports[`PageLoading should show a simple page loading component 1`] = `
class="euiPage euiPage--paddingMedium euiPage--grow"
style="height:calc(100vh - 50px)"
>
-
-
+
`;
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts
index 213e73a4b9534..a96a7454ea744 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts
@@ -95,7 +95,7 @@ export async function fetchCCRReadExceptions(
const { body: response } = await esClient.search(params);
const stats: CCRReadExceptionsStats[] = [];
- // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets
+ // @ts-expect-error declare aggegations type explicitly
const { buckets: remoteClusterBuckets = [] } = response.aggregations?.remote_clusters;
if (!remoteClusterBuckets?.length) {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts
index 0fb9dd5298e9e..9cb773c81923b 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts
@@ -25,7 +25,7 @@ describe('fetchCpuUsageNodeStats', () => {
it('fetch normal stats', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
@@ -79,7 +79,7 @@ describe('fetchCpuUsageNodeStats', () => {
it('fetch container stats', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
@@ -146,7 +146,7 @@ describe('fetchCpuUsageNodeStats', () => {
it('fetch properly return ccs', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts
index 8faf79fc4b59c..4766400891af5 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts
@@ -25,7 +25,7 @@ describe('fetchDiskUsageNodeStats', () => {
it('fetch normal stats', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts
index a51dccd727966..2e8b5c7478e15 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts
@@ -101,7 +101,7 @@ export async function fetchDiskUsageNodeStats(
const { body: response } = await esClient.search(params);
const stats: AlertDiskUsageNodeStats[] = [];
- // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets
+ // @ts-expect-error declare type for aggregations explicitly
const { buckets: clusterBuckets } = response.aggregations?.clusters;
if (!clusterBuckets?.length) {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts
index aab3f0101ef83..117894c0d823b 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts
@@ -105,7 +105,7 @@ export async function fetchIndexShardSize(
};
const { body: response } = await esClient.search(params);
- // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets
+ // @ts-expect-error declare aggegations type explicitly
const { buckets: clusterBuckets } = response.aggregations?.clusters;
const stats: IndexShardSizeStats[] = [];
if (!clusterBuckets?.length) {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts
index 2b966b16f2f5c..f9a03bb73d5fc 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts
@@ -23,7 +23,7 @@ describe('fetchKibanaVersions', () => {
it('fetch as expected', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
index: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts
index d7d4e6531f58e..5732fc00f009b 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts
@@ -23,7 +23,7 @@ describe('fetchLogstashVersions', () => {
it('fetch as expected', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
index: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts
index 245838541d435..46bb9c794a6a6 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts
@@ -94,7 +94,7 @@ export async function fetchMemoryUsageNodeStats(
const { body: response } = await esClient.search(params);
const stats: AlertMemoryUsageNodeStats[] = [];
- // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets
+ // @ts-expect-error declare type for aggregations explicitly
const { buckets: clusterBuckets } = response.aggregations?.clusters;
if (!clusterBuckets?.length) {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
index c8d15acf8ff73..980adb009ff8f 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
@@ -56,7 +56,7 @@ describe('fetchMissingMonitoringData', () => {
];
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
@@ -115,7 +115,7 @@ describe('fetchMissingMonitoringData', () => {
},
];
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts
index d1a343b9b3eef..5f867ca5b6edf 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts
@@ -90,7 +90,7 @@ export async function fetchNodesFromClusterStats(
const { body: response } = await esClient.search(params);
const nodes: AlertClusterStatsNodes[] = [];
- // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets
+ // @ts-expect-error declare type for aggregations explicitly
const clusterBuckets = response.aggregations?.clusters?.buckets;
if (!clusterBuckets?.length) {
return nodes;
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts
index db5943ca67031..954ec3877144f 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts
@@ -96,7 +96,7 @@ export async function fetchThreadPoolRejectionStats(
const { body: response } = await esClient.search(params);
const stats: AlertThreadPoolRejectionsStats[] = [];
- // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets
+ // @ts-expect-error declare type for aggregations explicitly
const { buckets: clusterBuckets } = response.aggregations?.clusters;
if (!clusterBuckets?.length) {
diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx
index 1636d08aa56e4..29a16590f3eb3 100644
--- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx
@@ -28,19 +28,18 @@ interface AllCasesProps {
export const AllCases = React.memo(({ userCanCrud }) => {
const {
cases: casesUi,
- application: { navigateToApp },
+ application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
const { formatUrl } = useFormatUrl(CASES_APP_ID);
+ const casesUrl = getUrlForApp(CASES_APP_ID);
return casesUi.getAllCases({
caseDetailsNavigation: {
href: ({ detailName, subCaseId }: AllCasesNavProps) => {
return formatUrl(getCaseDetailsUrl({ id: detailName, subCaseId }));
},
onClick: async ({ detailName, subCaseId, search }: AllCasesNavProps) =>
- navigateToApp(`${CASES_APP_ID}`, {
- path: getCaseDetailsUrl({ id: detailName, subCaseId }),
- }),
+ navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: detailName, subCaseId })}`),
},
configureCasesNavigation: {
href: formatUrl(getConfigureCasesUrl()),
@@ -48,9 +47,7 @@ export const AllCases = React.memo(({ userCanCrud }) => {
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp(`${CASES_APP_ID}`, {
- path: getConfigureCasesUrl(),
- });
+ return navigateToUrl(`${casesUrl}${getConfigureCasesUrl()}`);
},
},
createCaseNavigation: {
@@ -59,9 +56,7 @@ export const AllCases = React.memo(({ userCanCrud }) => {
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp(`${CASES_APP_ID}`, {
- path: getCreateCaseUrl(),
- });
+ return navigateToUrl(`${casesUrl}${getCreateCaseUrl()}`);
},
},
disableAlerts: true,
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 29b17cd426c58..fdd49ad17168d 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 cb7236b445be1..20bb57daf5841 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/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx
index 728333ac8c544..07d8019153a06 100644
--- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx
@@ -42,8 +42,10 @@ export interface CaseProps extends Props {
export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => {
const [caseTitle, setCaseTitle] = useState(null);
- const { cases: casesUi, application } = useKibana().services;
- const { navigateToApp } = application;
+ const {
+ cases: casesUi,
+ application: { getUrlForApp, navigateToUrl },
+ } = useKibana().services;
const allCasesLink = getCaseUrl();
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(allCasesLink);
@@ -79,6 +81,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
[caseId, formatUrl, subCaseId]
);
+ const casesUrl = getUrlForApp(CASES_APP_ID);
return casesUi.getCaseView({
allCasesNavigation: {
href: allCasesHref,
@@ -86,9 +89,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp(`${CASES_APP_ID}`, {
- path: allCasesLink,
- });
+ return navigateToUrl(casesUrl);
},
},
caseDetailsNavigation: {
@@ -97,9 +98,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp(`${CASES_APP_ID}`, {
- path: getCaseDetailsUrl({ id: caseId }),
- });
+ return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`);
},
},
caseId,
@@ -109,9 +108,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp(`${CASES_APP_ID}`, {
- path: configureCasesLink,
- });
+ return navigateToUrl(`${casesUrl}${configureCasesLink}`);
},
},
getCaseDetailHrefWithCommentId,
diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx
index c358067123747..f92f12c79a56d 100644
--- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx
@@ -49,7 +49,7 @@ describe('CreateCaseFlyout', () => {
);
- wrapper.find('.euiFlyout__closeButton').first().simulate('click');
+ wrapper.find(`[data-test-subj='euiFlyoutCloseButton']`).first().simulate('click');
expect(onCloseFlyout).toBeCalled();
});
});
diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx
index df29d02e8d830..b6cdcf3111672 100644
--- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx
@@ -5,8 +5,8 @@
* 2.0.
*/
-import React, { memo } from 'react';
-import styled from 'styled-components';
+import React, { memo, ReactNode } from 'react';
+import styled, { StyledComponent } from 'styled-components';
import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
import * as i18n from '../translations';
@@ -20,7 +20,11 @@ export interface CreateCaseModalProps {
onSuccess: (theCase: Case) => Promise;
}
-const StyledFlyout = styled(EuiFlyout)`
+// TODO: EUI team follow up on complex types and styled-components `styled`
+// https://github.com/elastic/eui/issues/4855
+const StyledFlyout: StyledComponent = styled(
+ EuiFlyout
+)`
${({ theme }) => `
z-index: ${theme.eui.euiZModal};
`}
diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx
index ec7511836328b..6dae88733fd49 100644
--- a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx
@@ -12,7 +12,7 @@ import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_rea
import { Create } from '.';
import { useKibana } from '../../../../utils/kibana_react';
import { basicCase } from '../../../../../../cases/public/containers/mock';
-import { CASES_APP_ID, CASES_OWNER } from '../constants';
+import { CASES_OWNER } from '../constants';
import { Case } from '../../../../../../cases/common';
import { getCaseDetailsUrl } from '../../../../pages/cases/links';
@@ -20,7 +20,8 @@ jest.mock('../../../../utils/kibana_react');
describe('Create case', () => {
const mockCreateCase = jest.fn();
- const mockNavigateToApp = jest.fn();
+ const mockNavigateToUrl = jest.fn();
+ const mockCasesUrl = 'https://elastic.co/app/observability/cases';
beforeEach(() => {
jest.resetAllMocks();
(useKibana as jest.Mock).mockReturnValue({
@@ -28,7 +29,7 @@ describe('Create case', () => {
cases: {
getCreateCase: mockCreateCase,
},
- application: { navigateToApp: mockNavigateToApp },
+ application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl },
},
});
});
@@ -52,7 +53,7 @@ describe('Create case', () => {
onCancel();
},
},
- application: { navigateToApp: mockNavigateToApp },
+ application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl },
},
});
mount(
@@ -61,7 +62,7 @@ describe('Create case', () => {
);
- await waitFor(() => expect(mockNavigateToApp).toHaveBeenCalledWith(`${CASES_APP_ID}`));
+ await waitFor(() => expect(mockNavigateToUrl).toHaveBeenCalledWith(`${mockCasesUrl}`));
});
it('should redirect to new case when posting the case', async () => {
@@ -72,7 +73,7 @@ describe('Create case', () => {
onSuccess(basicCase);
},
},
- application: { navigateToApp: mockNavigateToApp },
+ application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl },
},
});
mount(
@@ -82,9 +83,10 @@ describe('Create case', () => {
);
await waitFor(() =>
- expect(mockNavigateToApp).toHaveBeenNthCalledWith(1, `${CASES_APP_ID}`, {
- path: getCaseDetailsUrl({ id: basicCase.id }),
- })
+ expect(mockNavigateToUrl).toHaveBeenNthCalledWith(
+ 1,
+ `${mockCasesUrl}${getCaseDetailsUrl({ id: basicCase.id })}`
+ )
);
});
});
diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx
index d7e2daea2490b..a3ed234147314 100644
--- a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx
@@ -15,17 +15,18 @@ import { CASES_APP_ID, CASES_OWNER } from '../constants';
export const Create = React.memo(() => {
const {
cases,
- application: { navigateToApp },
+ application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
+ const casesUrl = getUrlForApp(CASES_APP_ID);
const onSuccess = useCallback(
- async ({ id }) =>
- navigateToApp(`${CASES_APP_ID}`, {
- path: getCaseDetailsUrl({ id }),
- }),
- [navigateToApp]
+ async ({ id }) => navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id })}`),
+ [casesUrl, navigateToUrl]
);
- const handleSetIsCancel = useCallback(() => navigateToApp(`${CASES_APP_ID}`), [navigateToApp]);
+ const handleSetIsCancel = useCallback(() => navigateToUrl(`${casesUrl}`), [
+ casesUrl,
+ navigateToUrl,
+ ]);
return (
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 1a5abe218edf5..a85b0bc744e66 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/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
index ea69a371cedae..3566835b1701c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
@@ -38,6 +38,12 @@ export function EmptyView({
emptyMessage = SELECTED_DATA_TYPE_FOR_REPORT;
}
+ if (!series) {
+ emptyMessage = i18n.translate('xpack.observability.expView.seriesEditor.notFound', {
+ defaultMessage: 'No series found. Please add a series.',
+ });
+ }
+
return (
{loading && (
@@ -77,7 +83,7 @@ export const EMPTY_LABEL = i18n.translate('xpack.observability.expView.seriesBui
export const CHOOSE_REPORT_DEFINITION = i18n.translate(
'xpack.observability.expView.seriesBuilder.emptyReportDefinition',
{
- defaultMessage: 'Select a report type to create a visualization.',
+ defaultMessage: 'Select a report definition to create a visualization.',
}
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
index af64e74bca89c..fe2953edd36d6 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
@@ -29,6 +29,7 @@ describe('FilterLabel', function () {
negate={false}
seriesId={'kpi-over-time'}
removeFilter={jest.fn()}
+ indexPattern={mockIndexPattern}
/>
);
@@ -52,6 +53,7 @@ describe('FilterLabel', function () {
negate={false}
seriesId={'kpi-over-time'}
removeFilter={removeFilter}
+ indexPattern={mockIndexPattern}
/>
);
@@ -74,6 +76,7 @@ describe('FilterLabel', function () {
negate={false}
seriesId={'kpi-over-time'}
removeFilter={removeFilter}
+ indexPattern={mockIndexPattern}
/>
);
@@ -99,6 +102,7 @@ describe('FilterLabel', function () {
negate={true}
seriesId={'kpi-over-time'}
removeFilter={jest.fn()}
+ indexPattern={mockIndexPattern}
/>
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
index 3d4ba6dc08c37..a08e777c5ea71 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
+import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
import { useSeriesFilters } from '../hooks/use_series_filters';
import { FilterValueLabel } from '../../filter_value_label/filter_value_label';
@@ -17,6 +17,7 @@ interface Props {
seriesId: string;
negate: boolean;
definitionFilter?: boolean;
+ indexPattern: IndexPattern;
removeFilter: (field: string, value: string, notVal: boolean) => void;
}
@@ -26,11 +27,10 @@ export function FilterLabel({
field,
value,
negate,
+ indexPattern,
removeFilter,
definitionFilter,
}: Props) {
- const { indexPattern } = useAppIndexPatternContext();
-
const { invertFilter } = useSeriesFilters({ seriesId });
return indexPattern ? (
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
index e119507860c5c..01e8d023ae96b 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
@@ -5,8 +5,15 @@
* 2.0.
*/
-import { ReportViewTypeId } from '../../types';
-import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames';
+import { ReportViewType } from '../../types';
+import {
+ CLS_FIELD,
+ FCP_FIELD,
+ FID_FIELD,
+ LCP_FIELD,
+ TBT_FIELD,
+ TRANSACTION_TIME_TO_FIRST_BYTE,
+} from './elasticsearch_fieldnames';
import {
AGENT_HOST_LABEL,
BROWSER_FAMILY_LABEL,
@@ -58,6 +65,7 @@ export const FieldLabels: Record = {
[TBT_FIELD]: TBT_LABEL,
[FID_FIELD]: FID_LABEL,
[CLS_FIELD]: CLS_LABEL,
+ [TRANSACTION_TIME_TO_FIRST_BYTE]: 'Page load time',
'monitor.id': MONITOR_ID_LABEL,
'monitor.status': MONITOR_STATUS_LABEL,
@@ -77,11 +85,11 @@ export const FieldLabels: Record = {
'http.request.method': REQUEST_METHOD,
};
-export const DataViewLabels: Record = {
- dist: PERF_DIST_LABEL,
- kpi: KPI_OVER_TIME_LABEL,
- cwv: CORE_WEB_VITALS_LABEL,
- mdd: DEVICE_DISTRIBUTION_LABEL,
+export const DataViewLabels: Record = {
+ 'data-distribution': PERF_DIST_LABEL,
+ 'kpi-over-time': KPI_OVER_TIME_LABEL,
+ 'core-web-vitals': CORE_WEB_VITALS_LABEL,
+ 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL,
};
export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
index 07342d976cbea..574a9f6a2bc10 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { AppDataType, ReportViewTypes } from '../types';
+import { AppDataType, ReportViewType } from '../types';
import { getRumDistributionConfig } from './rum/data_distribution_config';
import { getSyntheticsDistributionConfig } from './synthetics/data_distribution_config';
import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config';
@@ -17,7 +17,7 @@ import { getMobileKPIDistributionConfig } from './mobile/distribution_config';
import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config';
interface Props {
- reportType: keyof typeof ReportViewTypes;
+ reportType: ReportViewType;
indexPattern: IndexPattern;
dataType: AppDataType;
}
@@ -25,23 +25,23 @@ interface Props {
export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => {
switch (dataType) {
case 'ux':
- if (reportType === 'dist') {
+ if (reportType === 'data-distribution') {
return getRumDistributionConfig({ indexPattern });
}
- if (reportType === 'cwv') {
+ if (reportType === 'core-web-vitals') {
return getCoreWebVitalsConfig({ indexPattern });
}
return getKPITrendsLensConfig({ indexPattern });
case 'synthetics':
- if (reportType === 'dist') {
+ if (reportType === 'data-distribution') {
return getSyntheticsDistributionConfig({ indexPattern });
}
return getSyntheticsKPIConfig({ indexPattern });
case 'mobile':
- if (reportType === 'dist') {
+ if (reportType === 'data-distribution') {
return getMobileKPIDistributionConfig({ indexPattern });
}
- if (reportType === 'mdd') {
+ if (reportType === 'device-data-distribution') {
return getMobileDeviceDistributionConfig({ indexPattern });
}
return getMobileKPIConfig({ indexPattern });
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
index 8b21df64a3c91..5189a529bda8f 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
@@ -5,25 +5,37 @@
* 2.0.
*/
-import { LensAttributes } from './lens_attributes';
+import { LayerConfig, LensAttributes } from './lens_attributes';
import { mockAppIndexPattern, mockIndexPattern } from '../rtl_helpers';
import { getDefaultConfigs } from './default_configs';
import { sampleAttribute } from './test_data/sample_attribute';
-import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames';
+import { LCP_FIELD, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames';
+import { buildExistsFilter, buildPhrasesFilter } from './utils';
describe('Lens Attribute', () => {
mockAppIndexPattern();
const reportViewConfig = getDefaultConfigs({
- reportType: 'dist',
+ reportType: 'data-distribution',
dataType: 'ux',
indexPattern: mockIndexPattern,
});
+ reportViewConfig.filters?.push(...buildExistsFilter('transaction.type', mockIndexPattern));
+
let lnsAttr: LensAttributes;
+ const layerConfig: LayerConfig = {
+ reportConfig: reportViewConfig,
+ seriesType: 'line',
+ operationType: 'count',
+ indexPattern: mockIndexPattern,
+ reportDefinitions: {},
+ time: { from: 'now-15m', to: 'now' },
+ };
+
beforeEach(() => {
- lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {});
+ lnsAttr = new LensAttributes([layerConfig]);
});
it('should return expected json', function () {
@@ -31,7 +43,7 @@ describe('Lens Attribute', () => {
});
it('should return main y axis', function () {
- expect(lnsAttr.getMainYAxis()).toEqual({
+ expect(lnsAttr.getMainYAxis(layerConfig)).toEqual({
dataType: 'number',
isBucketed: false,
label: 'Pages loaded',
@@ -42,7 +54,7 @@ describe('Lens Attribute', () => {
});
it('should return expected field type', function () {
- expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual(
+ expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type', layerConfig))).toEqual(
JSON.stringify({
fieldMeta: {
count: 0,
@@ -60,7 +72,7 @@ describe('Lens Attribute', () => {
});
it('should return expected field type for custom field with default value', function () {
- expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual(
+ expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig))).toEqual(
JSON.stringify({
fieldMeta: {
count: 0,
@@ -79,11 +91,18 @@ describe('Lens Attribute', () => {
});
it('should return expected field type for custom field with passed value', function () {
- lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {
- 'performance.metric': [LCP_FIELD],
- });
+ const layerConfig1: LayerConfig = {
+ reportConfig: reportViewConfig,
+ seriesType: 'line',
+ operationType: 'count',
+ indexPattern: mockIndexPattern,
+ reportDefinitions: { 'performance.metric': [LCP_FIELD] },
+ time: { from: 'now-15m', to: 'now' },
+ };
- expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual(
+ lnsAttr = new LensAttributes([layerConfig1]);
+
+ expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig1))).toEqual(
JSON.stringify({
fieldMeta: {
count: 0,
@@ -102,7 +121,7 @@ describe('Lens Attribute', () => {
});
it('should return expected number range column', function () {
- expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({
+ expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({
dataType: 'number',
isBucketed: true,
label: 'Page load time',
@@ -124,7 +143,7 @@ describe('Lens Attribute', () => {
});
it('should return expected number operation column', function () {
- expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({
+ expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({
dataType: 'number',
isBucketed: true,
label: 'Page load time',
@@ -160,7 +179,7 @@ describe('Lens Attribute', () => {
});
it('should return main x axis', function () {
- expect(lnsAttr.getXAxis()).toEqual({
+ expect(lnsAttr.getXAxis(layerConfig, 'layer0')).toEqual({
dataType: 'number',
isBucketed: true,
label: 'Page load time',
@@ -182,38 +201,45 @@ describe('Lens Attribute', () => {
});
it('should return first layer', function () {
- expect(lnsAttr.getLayer()).toEqual({
- columnOrder: ['x-axis-column', 'y-axis-column'],
- columns: {
- 'x-axis-column': {
- dataType: 'number',
- isBucketed: true,
- label: 'Page load time',
- operationType: 'range',
- params: {
- maxBars: 'auto',
- ranges: [
- {
- from: 0,
- label: '',
- to: 1000,
- },
- ],
- type: 'histogram',
+ expect(lnsAttr.getLayers()).toEqual({
+ layer0: {
+ columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'],
+ columns: {
+ 'x-axis-column-layer0': {
+ dataType: 'number',
+ isBucketed: true,
+ label: 'Page load time',
+ operationType: 'range',
+ params: {
+ maxBars: 'auto',
+ ranges: [
+ {
+ from: 0,
+ label: '',
+ to: 1000,
+ },
+ ],
+ type: 'histogram',
+ },
+ scale: 'interval',
+ sourceField: 'transaction.duration.us',
+ },
+ 'y-axis-column-layer0': {
+ dataType: 'number',
+ isBucketed: false,
+ label: 'Pages loaded',
+ operationType: 'count',
+ scale: 'ratio',
+ sourceField: 'Records',
+ filter: {
+ language: 'kuery',
+ query:
+ 'transaction.type: page-load and processor.event: transaction and transaction.type : *',
+ },
},
- scale: 'interval',
- sourceField: 'transaction.duration.us',
- },
- 'y-axis-column': {
- dataType: 'number',
- isBucketed: false,
- label: 'Pages loaded',
- operationType: 'count',
- scale: 'ratio',
- sourceField: 'Records',
},
+ incompleteColumns: {},
},
- incompleteColumns: {},
});
});
@@ -225,12 +251,12 @@ describe('Lens Attribute', () => {
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
layers: [
{
- accessors: ['y-axis-column'],
- layerId: 'layer1',
+ accessors: ['y-axis-column-layer0'],
+ layerId: 'layer0',
palette: undefined,
seriesType: 'line',
- xAccessor: 'x-axis-column',
- yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
+ xAccessor: 'x-axis-column-layer0',
+ yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
},
],
legend: { isVisible: true, position: 'right' },
@@ -240,108 +266,52 @@ describe('Lens Attribute', () => {
});
});
- describe('ParseFilters function', function () {
- it('should parse default filters', function () {
- expect(lnsAttr.parseFilters()).toEqual([
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
- ]);
- });
-
- it('should parse default and ui filters', function () {
- lnsAttr = new LensAttributes(
- mockIndexPattern,
- reportViewConfig,
- 'line',
- [
- { field: SERVICE_NAME, values: ['elastic-co', 'kibana-front'] },
- { field: USER_AGENT_NAME, values: ['Firefox'], notValues: ['Chrome'] },
- ],
- 'count',
- {}
- );
+ describe('Layer breakdowns', function () {
+ it('should return breakdown column', function () {
+ const layerConfig1: LayerConfig = {
+ reportConfig: reportViewConfig,
+ seriesType: 'line',
+ operationType: 'count',
+ indexPattern: mockIndexPattern,
+ reportDefinitions: { 'performance.metric': [LCP_FIELD] },
+ breakdown: USER_AGENT_NAME,
+ time: { from: 'now-15m', to: 'now' },
+ };
- expect(lnsAttr.parseFilters()).toEqual([
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
- {
- meta: {
- index: 'apm-*',
- key: 'service.name',
- params: ['elastic-co', 'kibana-front'],
- type: 'phrases',
- value: 'elastic-co, kibana-front',
- },
- query: {
- bool: {
- minimum_should_match: 1,
- should: [
- {
- match_phrase: {
- 'service.name': 'elastic-co',
- },
- },
- {
- match_phrase: {
- 'service.name': 'kibana-front',
- },
- },
- ],
- },
- },
- },
- {
- meta: {
- index: 'apm-*',
- },
- query: {
- match_phrase: {
- 'user_agent.name': 'Firefox',
- },
- },
- },
- {
- meta: {
- index: 'apm-*',
- negate: true,
- },
- query: {
- match_phrase: {
- 'user_agent.name': 'Chrome',
- },
- },
- },
- ]);
- });
- });
+ lnsAttr = new LensAttributes([layerConfig1]);
- describe('Layer breakdowns', function () {
- it('should add breakdown column', function () {
- lnsAttr.addBreakdown(USER_AGENT_NAME);
+ lnsAttr.getBreakdownColumn({
+ sourceField: USER_AGENT_NAME,
+ layerId: 'layer0',
+ indexPattern: mockIndexPattern,
+ });
expect(lnsAttr.visualization.layers).toEqual([
{
- accessors: ['y-axis-column'],
- layerId: 'layer1',
+ accessors: ['y-axis-column-layer0'],
+ layerId: 'layer0',
palette: undefined,
seriesType: 'line',
- splitAccessor: 'break-down-column',
- xAccessor: 'x-axis-column',
- yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
+ splitAccessor: 'breakdown-column-layer0',
+ xAccessor: 'x-axis-column-layer0',
+ yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
},
]);
- expect(lnsAttr.layers.layer1).toEqual({
- columnOrder: ['x-axis-column', 'break-down-column', 'y-axis-column'],
+ expect(lnsAttr.layers.layer0).toEqual({
+ columnOrder: ['x-axis-column-layer0', 'breakdown-column-layer0', 'y-axis-column-layer0'],
columns: {
- 'break-down-column': {
+ 'breakdown-column-layer0': {
dataType: 'string',
isBucketed: true,
label: 'Top values of Browser family',
operationType: 'terms',
params: {
missingBucket: false,
- orderBy: { columnId: 'y-axis-column', type: 'column' },
+ orderBy: {
+ columnId: 'y-axis-column-layer0',
+ type: 'column',
+ },
orderDirection: 'desc',
otherBucket: true,
size: 10,
@@ -349,10 +319,10 @@ describe('Lens Attribute', () => {
scale: 'ordinal',
sourceField: 'user_agent.name',
},
- 'x-axis-column': {
+ 'x-axis-column-layer0': {
dataType: 'number',
isBucketed: true,
- label: 'Page load time',
+ label: 'Largest contentful paint',
operationType: 'range',
params: {
maxBars: 'auto',
@@ -360,62 +330,47 @@ describe('Lens Attribute', () => {
type: 'histogram',
},
scale: 'interval',
- sourceField: 'transaction.duration.us',
+ sourceField: 'transaction.marks.agent.largestContentfulPaint',
},
- 'y-axis-column': {
+ 'y-axis-column-layer0': {
dataType: 'number',
isBucketed: false,
label: 'Pages loaded',
operationType: 'count',
scale: 'ratio',
sourceField: 'Records',
+ filter: {
+ language: 'kuery',
+ query:
+ 'transaction.type: page-load and processor.event: transaction and transaction.type : *',
+ },
},
},
incompleteColumns: {},
});
});
+ });
- it('should remove breakdown column', function () {
- lnsAttr.addBreakdown(USER_AGENT_NAME);
-
- lnsAttr.removeBreakdown();
+ describe('Layer Filters', function () {
+ it('should return expected filters', function () {
+ reportViewConfig.filters?.push(
+ ...buildPhrasesFilter('service.name', ['elastic', 'kibana'], mockIndexPattern)
+ );
- expect(lnsAttr.visualization.layers).toEqual([
- {
- accessors: ['y-axis-column'],
- layerId: 'layer1',
- palette: undefined,
- seriesType: 'line',
- xAccessor: 'x-axis-column',
- yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
- },
- ]);
+ const layerConfig1: LayerConfig = {
+ reportConfig: reportViewConfig,
+ seriesType: 'line',
+ operationType: 'count',
+ indexPattern: mockIndexPattern,
+ reportDefinitions: { 'performance.metric': [LCP_FIELD] },
+ time: { from: 'now-15m', to: 'now' },
+ };
- expect(lnsAttr.layers.layer1.columnOrder).toEqual(['x-axis-column', 'y-axis-column']);
+ const filters = lnsAttr.getLayerFilters(layerConfig1, 2);
- expect(lnsAttr.layers.layer1.columns).toEqual({
- 'x-axis-column': {
- dataType: 'number',
- isBucketed: true,
- label: 'Page load time',
- operationType: 'range',
- params: {
- maxBars: 'auto',
- ranges: [{ from: 0, label: '', to: 1000 }],
- type: 'histogram',
- },
- scale: 'interval',
- sourceField: 'transaction.duration.us',
- },
- 'y-axis-column': {
- dataType: 'number',
- isBucketed: false,
- label: 'Pages loaded',
- operationType: 'count',
- scale: 'ratio',
- sourceField: 'Records',
- },
- });
+ expect(filters).toEqual(
+ '@timestamp >= now-15m and @timestamp <= now and transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)'
+ );
});
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
index 22ad18c663b32..208e8d8ba43c2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
@@ -27,13 +27,12 @@ import {
TermsIndexPatternColumn,
CardinalityIndexPatternColumn,
} from '../../../../../../lens/public';
-import {
- buildPhraseFilter,
- buildPhrasesFilter,
- IndexPattern,
-} from '../../../../../../../../src/plugins/data/common';
+import { urlFiltersToKueryString } from '../utils/stringify_kueries';
+import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants';
import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types';
+import { PersistableFilter } from '../../../../../../lens/common';
+import { parseAbsoluteDate } from '../series_date_picker/date_range_picker';
function getLayerReferenceName(layerId: string) {
return `indexpattern-datasource-layer-${layerId}`;
@@ -87,46 +86,50 @@ export const parseCustomFieldName = (
return { fieldName, columnType, columnFilters, timeScale, columnLabel };
};
-export class LensAttributes {
+export interface LayerConfig {
+ filters?: UrlFilter[];
+ reportConfig: DataSeries;
+ breakdown?: string;
+ seriesType?: SeriesType;
+ operationType?: OperationType;
+ reportDefinitions: URLReportDefinition;
+ time: { to: string; from: string };
indexPattern: IndexPattern;
+}
+
+export class LensAttributes {
layers: Record;
visualization: XYState;
- filters: UrlFilter[];
- seriesType: SeriesType;
- reportViewConfig: DataSeries;
- reportDefinitions: URLReportDefinition;
- breakdownSource?: string;
+ layerConfigs: LayerConfig[];
- constructor(
- indexPattern: IndexPattern,
- reportViewConfig: DataSeries,
- seriesType?: SeriesType,
- filters?: UrlFilter[],
- operationType?: OperationType,
- reportDefinitions?: URLReportDefinition,
- breakdownSource?: string
- ) {
- this.indexPattern = indexPattern;
+ constructor(layerConfigs: LayerConfig[]) {
this.layers = {};
- this.filters = filters ?? [];
- this.reportDefinitions = reportDefinitions ?? {};
- this.breakdownSource = breakdownSource;
-
- if (operationType) {
- reportViewConfig.yAxisColumns.forEach((yAxisColumn) => {
- if (typeof yAxisColumn.operationType !== undefined) {
- yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType'];
- }
- });
- }
- this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType;
- this.reportViewConfig = reportViewConfig;
- this.layers.layer1 = this.getLayer();
+
+ layerConfigs.forEach(({ reportConfig, operationType }) => {
+ if (operationType) {
+ reportConfig.yAxisColumns.forEach((yAxisColumn) => {
+ if (typeof yAxisColumn.operationType !== undefined) {
+ yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType'];
+ }
+ });
+ }
+ });
+
+ this.layerConfigs = layerConfigs;
+ this.layers = this.getLayers();
this.visualization = this.getXyState();
}
- getBreakdownColumn(sourceField: string): TermsIndexPatternColumn {
- const fieldMeta = this.indexPattern.getFieldByName(sourceField);
+ getBreakdownColumn({
+ sourceField,
+ layerId,
+ indexPattern,
+ }: {
+ sourceField: string;
+ layerId: string;
+ indexPattern: IndexPattern;
+ }): TermsIndexPatternColumn {
+ const fieldMeta = indexPattern.getFieldByName(sourceField);
return {
sourceField,
@@ -136,8 +139,8 @@ export class LensAttributes {
scale: 'ordinal',
isBucketed: true,
params: {
+ orderBy: { type: 'column', columnId: `y-axis-column-${layerId}` },
size: 10,
- orderBy: { type: 'column', columnId: 'y-axis-column' },
orderDirection: 'desc',
otherBucket: true,
missingBucket: false,
@@ -145,36 +148,14 @@ export class LensAttributes {
};
}
- addBreakdown(sourceField: string) {
- const { xAxisColumn } = this.reportViewConfig;
- if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) {
- // do nothing since this will be used a x axis source
- return;
- }
- this.layers.layer1.columns['break-down-column'] = this.getBreakdownColumn(sourceField);
-
- this.layers.layer1.columnOrder = [
- 'x-axis-column',
- 'break-down-column',
- 'y-axis-column',
- ...Object.keys(this.getChildYAxises()),
- ];
-
- this.visualization.layers[0].splitAccessor = 'break-down-column';
- }
-
- removeBreakdown() {
- delete this.layers.layer1.columns['break-down-column'];
-
- this.layers.layer1.columnOrder = ['x-axis-column', 'y-axis-column'];
-
- this.visualization.layers[0].splitAccessor = undefined;
- }
-
- getNumberRangeColumn(sourceField: string, label?: string): RangeIndexPatternColumn {
+ getNumberRangeColumn(
+ sourceField: string,
+ reportViewConfig: DataSeries,
+ label?: string
+ ): RangeIndexPatternColumn {
return {
sourceField,
- label: this.reportViewConfig.labels[sourceField] ?? label,
+ label: reportViewConfig.labels[sourceField] ?? label,
dataType: 'number',
operationType: 'range',
isBucketed: true,
@@ -187,16 +168,36 @@ export class LensAttributes {
};
}
- getCardinalityColumn(sourceField: string, label?: string) {
- return this.getNumberOperationColumn(sourceField, 'unique_count', label);
+ getCardinalityColumn({
+ sourceField,
+ label,
+ reportViewConfig,
+ }: {
+ sourceField: string;
+ label?: string;
+ reportViewConfig: DataSeries;
+ }) {
+ return this.getNumberOperationColumn({
+ sourceField,
+ operationType: 'unique_count',
+ label,
+ reportViewConfig,
+ });
}
- getNumberColumn(
- sourceField: string,
- columnType?: string,
- operationType?: string,
- label?: string
- ) {
+ getNumberColumn({
+ reportViewConfig,
+ label,
+ sourceField,
+ columnType,
+ operationType,
+ }: {
+ sourceField: string;
+ columnType?: string;
+ operationType?: string;
+ label?: string;
+ reportViewConfig: DataSeries;
+ }) {
if (columnType === 'operation' || operationType) {
if (
operationType === 'median' ||
@@ -204,48 +205,58 @@ export class LensAttributes {
operationType === 'sum' ||
operationType === 'unique_count'
) {
- return this.getNumberOperationColumn(sourceField, operationType, label);
+ return this.getNumberOperationColumn({
+ sourceField,
+ operationType,
+ label,
+ reportViewConfig,
+ });
}
if (operationType?.includes('th')) {
- return this.getPercentileNumberColumn(sourceField, operationType);
+ return this.getPercentileNumberColumn(sourceField, operationType, reportViewConfig!);
}
}
- return this.getNumberRangeColumn(sourceField, label);
+ return this.getNumberRangeColumn(sourceField, reportViewConfig!, label);
}
- getNumberOperationColumn(
- sourceField: string,
- operationType: 'average' | 'median' | 'sum' | 'unique_count',
- label?: string
- ):
+ getNumberOperationColumn({
+ sourceField,
+ label,
+ reportViewConfig,
+ operationType,
+ }: {
+ sourceField: string;
+ operationType: 'average' | 'median' | 'sum' | 'unique_count';
+ label?: string;
+ reportViewConfig: DataSeries;
+ }):
| AvgIndexPatternColumn
| MedianIndexPatternColumn
| SumIndexPatternColumn
| CardinalityIndexPatternColumn {
return {
...buildNumberColumn(sourceField),
- label:
- label ||
- i18n.translate('xpack.observability.expView.columns.operation.label', {
- defaultMessage: '{operationType} of {sourceField}',
- values: {
- sourceField: this.reportViewConfig.labels[sourceField],
- operationType: capitalize(operationType),
- },
- }),
+ label: i18n.translate('xpack.observability.expView.columns.operation.label', {
+ defaultMessage: '{operationType} of {sourceField}',
+ values: {
+ sourceField: label || reportViewConfig.labels[sourceField],
+ operationType: capitalize(operationType),
+ },
+ }),
operationType,
};
}
getPercentileNumberColumn(
sourceField: string,
- percentileValue: string
+ percentileValue: string,
+ reportViewConfig: DataSeries
): PercentileIndexPatternColumn {
return {
...buildNumberColumn(sourceField),
label: i18n.translate('xpack.observability.expView.columns.label', {
defaultMessage: '{percentileValue} percentile of {sourceField}',
- values: { sourceField: this.reportViewConfig.labels[sourceField], percentileValue },
+ values: { sourceField: reportViewConfig.labels[sourceField], percentileValue },
}),
operationType: 'percentile',
params: { percentile: Number(percentileValue.split('th')[0]) },
@@ -268,7 +279,7 @@ export class LensAttributes {
return {
operationType: 'terms',
sourceField,
- label: label || 'Top values of ' + sourceField,
+ label: 'Top values of ' + label || sourceField,
dataType: 'string',
isBucketed: true,
scale: 'ordinal',
@@ -283,30 +294,45 @@ export class LensAttributes {
};
}
- getXAxis() {
- const { xAxisColumn } = this.reportViewConfig;
+ getXAxis(layerConfig: LayerConfig, layerId: string) {
+ const { xAxisColumn } = layerConfig.reportConfig;
if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) {
- return this.getBreakdownColumn(this.breakdownSource || this.reportViewConfig.breakdowns[0]);
+ return this.getBreakdownColumn({
+ layerId,
+ indexPattern: layerConfig.indexPattern,
+ sourceField: layerConfig.breakdown || layerConfig.reportConfig.breakdowns[0],
+ });
}
- return this.getColumnBasedOnType(xAxisColumn.sourceField!, undefined, xAxisColumn.label);
+ return this.getColumnBasedOnType({
+ layerConfig,
+ label: xAxisColumn.label,
+ sourceField: xAxisColumn.sourceField!,
+ });
}
- getColumnBasedOnType(
- sourceField: string,
- operationType?: OperationType,
- label?: string,
- colIndex?: number
- ) {
+ getColumnBasedOnType({
+ sourceField,
+ label,
+ layerConfig,
+ operationType,
+ colIndex,
+ }: {
+ sourceField: string;
+ operationType?: OperationType;
+ label?: string;
+ layerConfig: LayerConfig;
+ colIndex?: number;
+ }) {
const {
fieldMeta,
columnType,
fieldName,
- columnFilters,
- timeScale,
columnLabel,
- } = this.getFieldMeta(sourceField);
+ timeScale,
+ columnFilters,
+ } = this.getFieldMeta(sourceField, layerConfig);
const { type: fieldType } = fieldMeta ?? {};
if (columnType === TERMS_COLUMN) {
@@ -325,47 +351,76 @@ export class LensAttributes {
return this.getDateHistogramColumn(fieldName);
}
if (fieldType === 'number') {
- return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label);
+ return this.getNumberColumn({
+ sourceField: fieldName,
+ columnType,
+ operationType,
+ label: columnLabel || label,
+ reportViewConfig: layerConfig.reportConfig,
+ });
}
if (operationType === 'unique_count') {
- return this.getCardinalityColumn(fieldName, columnLabel || label);
+ return this.getCardinalityColumn({
+ sourceField: fieldName,
+ label: columnLabel || label,
+ reportViewConfig: layerConfig.reportConfig,
+ });
}
// FIXME review my approach again
return this.getDateHistogramColumn(fieldName);
}
- getCustomFieldName(sourceField: string) {
- return parseCustomFieldName(sourceField, this.reportViewConfig, this.reportDefinitions);
+ getCustomFieldName({
+ sourceField,
+ layerConfig,
+ }: {
+ sourceField: string;
+ layerConfig: LayerConfig;
+ }) {
+ return parseCustomFieldName(
+ sourceField,
+ layerConfig.reportConfig,
+ layerConfig.reportDefinitions
+ );
}
- getFieldMeta(sourceField: string) {
+ getFieldMeta(sourceField: string, layerConfig: LayerConfig) {
const {
fieldName,
columnType,
+ columnLabel,
columnFilters,
timeScale,
- columnLabel,
- } = this.getCustomFieldName(sourceField);
+ } = this.getCustomFieldName({
+ sourceField,
+ layerConfig,
+ });
- const fieldMeta = this.indexPattern.getFieldByName(fieldName);
+ const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName);
- return { fieldMeta, fieldName, columnType, columnFilters, timeScale, columnLabel };
+ return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale };
}
- getMainYAxis() {
- const { sourceField, operationType, label } = this.reportViewConfig.yAxisColumns[0];
+ getMainYAxis(layerConfig: LayerConfig) {
+ const { sourceField, operationType, label } = layerConfig.reportConfig.yAxisColumns[0];
if (sourceField === 'Records' || !sourceField) {
return this.getRecordsColumn(label);
}
- return this.getColumnBasedOnType(sourceField!, operationType, label, 0);
+ return this.getColumnBasedOnType({
+ sourceField,
+ operationType,
+ label,
+ layerConfig,
+ colIndex: 0,
+ });
}
- getChildYAxises() {
+ getChildYAxises(layerConfig: LayerConfig) {
const lensColumns: Record = {};
- const yAxisColumns = this.reportViewConfig.yAxisColumns;
+ const yAxisColumns = layerConfig.reportConfig.yAxisColumns;
// 1 means there is only main y axis
if (yAxisColumns.length === 1) {
return lensColumns;
@@ -373,12 +428,13 @@ export class LensAttributes {
for (let i = 1; i < yAxisColumns.length; i++) {
const { sourceField, operationType, label } = yAxisColumns[i];
- lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType(
- sourceField!,
+ lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType({
+ sourceField: sourceField!,
operationType,
label,
- i
- );
+ layerConfig,
+ colIndex: i,
+ });
}
return lensColumns;
}
@@ -396,20 +452,139 @@ export class LensAttributes {
scale: 'ratio',
sourceField: 'Records',
filter: columnFilter,
- timeScale,
+ ...(timeScale ? { timeScale } : {}),
} as CountIndexPatternColumn;
}
- getLayer() {
- return {
- columnOrder: ['x-axis-column', 'y-axis-column', ...Object.keys(this.getChildYAxises())],
- columns: {
- 'x-axis-column': this.getXAxis(),
- 'y-axis-column': this.getMainYAxis(),
- ...this.getChildYAxises(),
- },
- incompleteColumns: {},
- };
+ getLayerFilters(layerConfig: LayerConfig, totalLayers: number) {
+ const {
+ filters,
+ time: { from, to },
+ reportConfig: { filters: layerFilters, reportType },
+ } = layerConfig;
+ let baseFilters = '';
+ if (reportType !== 'kpi-over-time' && totalLayers > 1) {
+ // for kpi over time, we don't need to add time range filters
+ // since those are essentially plotted along the x-axis
+ baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`;
+ }
+
+ layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => {
+ const qFilter = filter as PersistableFilter;
+ if (qFilter.query?.match_phrase) {
+ const fieldName = Object.keys(qFilter.query.match_phrase)[0];
+ const kql = `${fieldName}: ${qFilter.query.match_phrase[fieldName]}`;
+ if (baseFilters.length > 0) {
+ baseFilters += ` and ${kql}`;
+ } else {
+ baseFilters += kql;
+ }
+ }
+ if (qFilter.query?.bool?.should) {
+ const values: string[] = [];
+ let fieldName = '';
+ qFilter.query?.bool.should.forEach((ft: PersistableFilter['query']['match_phrase']) => {
+ if (ft.match_phrase) {
+ fieldName = Object.keys(ft.match_phrase)[0];
+ values.push(ft.match_phrase[fieldName]);
+ }
+ });
+
+ const kueryString = `${fieldName}: (${values.join(' or ')})`;
+
+ if (baseFilters.length > 0) {
+ baseFilters += ` and ${kueryString}`;
+ } else {
+ baseFilters += kueryString;
+ }
+ }
+ const existFilter = filter as ExistsFilter;
+
+ if (existFilter.exists) {
+ const fieldName = existFilter.exists.field;
+ const kql = `${fieldName} : *`;
+ if (baseFilters.length > 0) {
+ baseFilters += ` and ${kql}`;
+ } else {
+ baseFilters += kql;
+ }
+ }
+ });
+
+ const rFilters = urlFiltersToKueryString(filters ?? []);
+ if (!baseFilters) {
+ return rFilters;
+ }
+ if (!rFilters) {
+ return baseFilters;
+ }
+ return `${rFilters} and ${baseFilters}`;
+ }
+
+ getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) {
+ if (index === 0 || mainLayerConfig.reportConfig.reportType !== 'kpi-over-time') {
+ return null;
+ }
+
+ const {
+ time: { from: mainFrom },
+ } = mainLayerConfig;
+
+ const {
+ time: { from },
+ } = layerConfig;
+
+ const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days');
+ if (inDays > 1) {
+ return inDays + 'd';
+ }
+ const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours');
+ return inHours + 'h';
+ }
+
+ getLayers() {
+ const layers: Record = {};
+ const layerConfigs = this.layerConfigs;
+
+ layerConfigs.forEach((layerConfig, index) => {
+ const { breakdown } = layerConfig;
+
+ const layerId = `layer${index}`;
+ const columnFilter = this.getLayerFilters(layerConfig, layerConfigs.length);
+ const timeShift = this.getTimeShift(this.layerConfigs[0], layerConfig, index);
+ const mainYAxis = this.getMainYAxis(layerConfig);
+ layers[layerId] = {
+ columnOrder: [
+ `x-axis-column-${layerId}`,
+ ...(breakdown ? [`breakdown-column-${layerId}`] : []),
+ `y-axis-column-${layerId}`,
+ ...Object.keys(this.getChildYAxises(layerConfig)),
+ ],
+ columns: {
+ [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId),
+ [`y-axis-column-${layerId}`]: {
+ ...mainYAxis,
+ label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label,
+ filter: { query: columnFilter, language: 'kuery' },
+ ...(timeShift ? { timeShift } : {}),
+ },
+ ...(breakdown && breakdown !== USE_BREAK_DOWN_COLUMN
+ ? // do nothing since this will be used a x axis source
+ {
+ [`breakdown-column-${layerId}`]: this.getBreakdownColumn({
+ layerId,
+ sourceField: breakdown,
+ indexPattern: layerConfig.indexPattern,
+ }),
+ }
+ : {}),
+ ...this.getChildYAxises(layerConfig),
+ },
+ incompleteColumns: {},
+ };
+ });
+
+ return layers;
}
getXyState(): XYState {
@@ -422,71 +597,48 @@ export class LensAttributes {
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
preferredSeriesType: 'line',
- layers: [
- {
- accessors: ['y-axis-column', ...Object.keys(this.getChildYAxises())],
- layerId: 'layer1',
- seriesType: this.seriesType ?? 'line',
- palette: this.reportViewConfig.palette,
- yConfig: this.reportViewConfig.yConfig || [
- { forAccessor: 'y-axis-column', color: 'green' },
- ],
- xAccessor: 'x-axis-column',
- },
- ],
- ...(this.reportViewConfig.yTitle ? { yTitle: this.reportViewConfig.yTitle } : {}),
+ layers: this.layerConfigs.map((layerConfig, index) => ({
+ accessors: [
+ `y-axis-column-layer${index}`,
+ ...Object.keys(this.getChildYAxises(layerConfig)),
+ ],
+ layerId: `layer${index}`,
+ seriesType: layerConfig.seriesType || layerConfig.reportConfig.defaultSeriesType,
+ palette: layerConfig.reportConfig.palette,
+ yConfig: layerConfig.reportConfig.yConfig || [
+ { forAccessor: `y-axis-column-layer${index}` },
+ ],
+ xAccessor: `x-axis-column-layer${index}`,
+ ...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}),
+ })),
+ ...(this.layerConfigs[0].reportConfig.yTitle
+ ? { yTitle: this.layerConfigs[0].reportConfig.yTitle }
+ : {}),
};
}
- parseFilters() {
- const defaultFilters = this.reportViewConfig.filters ?? [];
- const parsedFilters = this.reportViewConfig.filters ? [...defaultFilters] : [];
-
- this.filters.forEach(({ field, values = [], notValues = [] }) => {
- const fieldMeta = this.indexPattern.fields.find((fieldT) => fieldT.name === field)!;
-
- if (values?.length > 0) {
- if (values?.length > 1) {
- const multiFilter = buildPhrasesFilter(fieldMeta, values, this.indexPattern);
- parsedFilters.push(multiFilter);
- } else {
- const filter = buildPhraseFilter(fieldMeta, values[0], this.indexPattern);
- parsedFilters.push(filter);
- }
- }
-
- if (notValues?.length > 0) {
- if (notValues?.length > 1) {
- const multiFilter = buildPhrasesFilter(fieldMeta, notValues, this.indexPattern);
- multiFilter.meta.negate = true;
- parsedFilters.push(multiFilter);
- } else {
- const filter = buildPhraseFilter(fieldMeta, notValues[0], this.indexPattern);
- filter.meta.negate = true;
- parsedFilters.push(filter);
- }
- }
- });
-
- return parsedFilters;
- }
+ parseFilters() {}
getJSON(): TypedLensByValueInput['attributes'] {
+ const uniqueIndexPatternsIds = Array.from(
+ new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)])
+ );
+
return {
title: 'Prefilled from exploratory view app',
description: '',
visualizationType: 'lnsXY',
references: [
- {
- id: this.indexPattern.id!,
+ ...uniqueIndexPatternsIds.map((patternId) => ({
+ id: patternId!,
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
- },
- {
- id: this.indexPattern.id!,
- name: getLayerReferenceName('layer1'),
+ })),
+ ...this.layerConfigs.map(({ indexPattern }, index) => ({
+ id: indexPattern.id!,
+ name: getLayerReferenceName(`layer${index}`),
type: 'index-pattern',
- },
+ })),
],
state: {
datasourceStates: {
@@ -496,7 +648,7 @@ export class LensAttributes {
},
visualization: this.visualization,
query: { query: '', language: 'kuery' },
- filters: this.parseFilters(),
+ filters: [],
},
};
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
index 6f9806660e489..e1cb5a0370fb2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
@@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields';
export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries {
return {
- reportType: 'mobile-device-distribution',
+ reportType: 'device-data-distribution',
defaultSeriesType: 'bar',
seriesTypes: ['bar', 'bar_horizontal'],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
index 854f844db047d..b958c0dd71528 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
@@ -10,10 +10,10 @@ import { FieldLabels, RECORDS_FIELD } from '../constants';
import { buildExistsFilter } from '../utils';
import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels';
-export function getSyntheticsDistributionConfig({ indexPattern }: ConfigProps): DataSeries {
+export function getSyntheticsDistributionConfig({ series, indexPattern }: ConfigProps): DataSeries {
return {
reportType: 'data-distribution',
- defaultSeriesType: 'line',
+ defaultSeriesType: series?.seriesType || 'line',
seriesTypes: [],
xAxisColumn: {
sourceField: 'performance.metric',
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
index 9b299e7d70bcc..edf2a42415820 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
@@ -10,16 +10,16 @@ export const sampleAttribute = {
visualizationType: 'lnsXY',
references: [
{ id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' },
- { id: 'apm-*', name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern' },
+ { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' },
],
state: {
datasourceStates: {
indexpattern: {
layers: {
- layer1: {
- columnOrder: ['x-axis-column', 'y-axis-column'],
+ layer0: {
+ columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'],
columns: {
- 'x-axis-column': {
+ 'x-axis-column-layer0': {
sourceField: 'transaction.duration.us',
label: 'Page load time',
dataType: 'number',
@@ -32,13 +32,18 @@ export const sampleAttribute = {
maxBars: 'auto',
},
},
- 'y-axis-column': {
+ 'y-axis-column-layer0': {
dataType: 'number',
isBucketed: false,
label: 'Pages loaded',
operationType: 'count',
scale: 'ratio',
sourceField: 'Records',
+ filter: {
+ language: 'kuery',
+ query:
+ 'transaction.type: page-load and processor.event: transaction and transaction.type : *',
+ },
},
},
incompleteColumns: {},
@@ -57,18 +62,15 @@ export const sampleAttribute = {
preferredSeriesType: 'line',
layers: [
{
- accessors: ['y-axis-column'],
- layerId: 'layer1',
+ accessors: ['y-axis-column-layer0'],
+ layerId: 'layer0',
seriesType: 'line',
- yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }],
- xAccessor: 'x-axis-column',
+ yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
+ xAccessor: 'x-axis-column-layer0',
},
],
},
query: { query: '', language: 'kuery' },
- filters: [
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
- ],
+ filters: [],
},
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
index fc60800bc4403..9b1e7ec141ca2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
@@ -5,11 +5,12 @@
* 2.0.
*/
import rison, { RisonValue } from 'rison-node';
+import type { SeriesUrl, UrlFilter } from '../types';
import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage';
-import type { SeriesUrl } from '../types';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
-import { esFilters } from '../../../../../../../../src/plugins/data/public';
+import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public';
import { URL_KEYS } from './constants/url_constants';
+import { PersistableFilter } from '../../../../../../lens/common';
export function convertToShortUrl(series: SeriesUrl) {
const {
@@ -51,7 +52,7 @@ export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') {
}
export function buildPhraseFilter(field: string, value: string, indexPattern: IIndexPattern) {
- const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field);
+ const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field);
if (fieldMeta) {
return [esFilters.buildPhraseFilter(fieldMeta, value, indexPattern)];
}
@@ -59,7 +60,7 @@ export function buildPhraseFilter(field: string, value: string, indexPattern: II
}
export function buildPhrasesFilter(field: string, value: string[], indexPattern: IIndexPattern) {
- const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field);
+ const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field);
if (fieldMeta) {
return [esFilters.buildPhrasesFilter(fieldMeta, value, indexPattern)];
}
@@ -67,9 +68,38 @@ export function buildPhrasesFilter(field: string, value: string[], indexPattern:
}
export function buildExistsFilter(field: string, indexPattern: IIndexPattern) {
- const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field);
+ const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field);
if (fieldMeta) {
return [esFilters.buildExistsFilter(fieldMeta, indexPattern)];
}
return [];
}
+
+type FiltersType = PersistableFilter[] | ExistsFilter[];
+
+export function urlFilterToPersistedFilter({
+ urlFilters,
+ initFilters,
+ indexPattern,
+}: {
+ urlFilters: UrlFilter[];
+ initFilters: FiltersType;
+ indexPattern: IIndexPattern;
+}) {
+ const parsedFilters: FiltersType = initFilters ? [...initFilters] : [];
+
+ urlFilters.forEach(({ field, values = [], notValues = [] }) => {
+ if (values?.length > 0) {
+ const filter = buildPhrasesFilter(field, values, indexPattern);
+ parsedFilters.push(...filter);
+ }
+
+ if (notValues?.length > 0) {
+ const filter = buildPhrasesFilter(field, notValues, indexPattern)[0];
+ filter.meta.negate = true;
+ parsedFilters.push(filter);
+ }
+ });
+
+ return parsedFilters;
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
index 779049601bd6d..989ebf17c2062 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
@@ -51,8 +51,9 @@ describe('ExploratoryView', () => {
const initSeries = {
data: {
'ux-series': {
+ isNew: true,
dataType: 'ux' as const,
- reportType: 'dist' as const,
+ reportType: 'data-distribution' as const,
breakdown: 'user_agent .name',
reportDefinitions: { 'service.name': ['elastic-co'] },
time: { from: 'now-15m', to: 'now' },
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
index 329ed20ffed3d..ad85ecab968b2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
@@ -5,9 +5,10 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useEffect, useRef, useState, useCallback } from 'react';
import { EuiPanel, EuiTitle } from '@elastic/eui';
import styled from 'styled-components';
+import { isEmpty } from 'lodash';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { ExploratoryViewHeader } from './header/header';
@@ -17,10 +18,37 @@ import { EmptyView } from './components/empty_view';
import { TypedLensByValueInput } from '../../../../../lens/public';
import { useAppIndexPatternContext } from './hooks/use_app_index_pattern';
import { SeriesBuilder } from './series_builder/series_builder';
+import { SeriesUrl } from './types';
+
+export const combineTimeRanges = (
+ allSeries: Record,
+ firstSeries?: SeriesUrl
+) => {
+ let to: string = '';
+ let from: string = '';
+ if (firstSeries?.reportType === 'kpi-over-time') {
+ return firstSeries.time;
+ }
+ Object.values(allSeries ?? {}).forEach((series) => {
+ if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) {
+ const seriesTo = new Date(series.time.to);
+ const seriesFrom = new Date(series.time.from);
+ if (!to || seriesTo > new Date(to)) {
+ to = series.time.to;
+ }
+ if (!from || seriesFrom < new Date(from)) {
+ from = series.time.from;
+ }
+ }
+ });
+ return { to, from };
+};
export function ExploratoryView({
saveAttributes,
+ multiSeries,
}: {
+ multiSeries?: boolean;
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
}) {
const {
@@ -33,6 +61,8 @@ export function ExploratoryView({
const [height, setHeight] = useState('100vh');
const [seriesId, setSeriesId] = useState('');
+ const [lastUpdated, setLastUpdated] = useState();
+
const [lensAttributes, setLensAttributes] = useState(
null
);
@@ -47,9 +77,7 @@ export function ExploratoryView({
setSeriesId(firstSeriesId);
}, [allSeries, firstSeriesId]);
- const lensAttributesT = useLensAttributes({
- seriesId,
- });
+ const lensAttributesT = useLensAttributes();
const setHeightOffset = () => {
if (seriesBuilderRef?.current && wrapperRef.current) {
@@ -60,10 +88,12 @@ export function ExploratoryView({
};
useEffect(() => {
- if (series?.dataType) {
- loadIndexPattern({ dataType: series?.dataType });
- }
- }, [series?.dataType, loadIndexPattern]);
+ Object.values(allSeries).forEach((seriesT) => {
+ loadIndexPattern({
+ dataType: seriesT.dataType,
+ });
+ });
+ }, [allSeries, loadIndexPattern]);
useEffect(() => {
setLensAttributes(lensAttributesT);
@@ -72,47 +102,62 @@ export function ExploratoryView({
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [JSON.stringify(lensAttributesT ?? {}), series?.reportType, series?.time?.from]);
+ }, [JSON.stringify(lensAttributesT ?? {})]);
useEffect(() => {
setHeightOffset();
});
+ const timeRange = combineTimeRanges(allSeries, series);
+
+ const onLensLoad = useCallback(() => {
+ setLastUpdated(Date.now());
+ }, []);
+
+ const onBrushEnd = useCallback(
+ ({ range }: { range: number[] }) => {
+ if (series?.reportType !== 'data-distribution') {
+ setSeries(seriesId, {
+ ...series,
+ time: {
+ from: new Date(range[0]).toISOString(),
+ to: new Date(range[1]).toISOString(),
+ },
+ });
+ } else {
+ notifications?.toasts.add(
+ i18n.translate('xpack.observability.exploratoryView.noBrusing', {
+ defaultMessage: 'Zoom by brush selection is only available on time series charts.',
+ })
+ );
+ }
+ },
+ [notifications?.toasts, series, seriesId, setSeries]
+ );
+
return (
{lens ? (
<>
- {lensAttributes && seriesId && series?.reportType && series?.time ? (
+ {lensAttributes && timeRange.to && timeRange.from ? (
{
- if (series?.reportType !== 'dist') {
- setSeries(seriesId, {
- ...series,
- time: {
- from: new Date(range[0]).toISOString(),
- to: new Date(range[1]).toISOString(),
- },
- });
- } else {
- notifications?.toasts.add(
- i18n.translate('xpack.observability.exploratoryView.noBrusing', {
- defaultMessage:
- 'Zoom by brush selection is only available on time series charts.',
- })
- );
- }
- }}
+ onLoad={onLensLoad}
+ onBrushEnd={onBrushEnd}
/>
) : (
)}
-
+
>
) : (
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx
index 1dedc4142f174..8cd8977fcf741 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx
@@ -26,7 +26,7 @@ describe('ExploratoryViewHeader', function () {
data: {
'uptime-pings-histogram': {
dataType: 'synthetics' as const,
- reportType: 'kpi' as const,
+ reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
index 3e02207e26272..dbe9cd163451d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
@@ -13,6 +13,7 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ
import { DataViewLabels } from '../configurations/constants';
import { ObservabilityAppServices } from '../../../../application/types';
import { useSeriesStorage } from '../hooks/use_series_storage';
+import { combineTimeRanges } from '../exploratory_view';
interface Props {
seriesId: string;
@@ -24,7 +25,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
const { lens } = kServices;
- const { getSeries } = useSeriesStorage();
+ const { getSeries, allSeries } = useSeriesStorage();
const series = getSeries(seriesId);
@@ -32,6 +33,8 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
const LensSaveModalComponent = lens.SaveModalComponent;
+ const timeRange = combineTimeRanges(allSeries, series);
+
return (
<>
@@ -63,7 +66,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
lens.navigateToPrefilledEditor(
{
id: '',
- timeRange: series.time,
+ timeRange,
attributes: lensAttributes,
},
true
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
index 4259bb778e511..7a5f12a72b1f0 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
@@ -15,7 +15,6 @@ import { getDataHandler } from '../../../../data_handler';
export interface IIndexPatternContext {
loading: boolean;
- selectedApp: AppDataType;
indexPatterns: IndexPatternState;
hasAppData: HasAppDataState;
loadIndexPattern: (params: { dataType: AppDataType }) => void;
@@ -29,10 +28,10 @@ interface ProviderProps {
type HasAppDataState = Record;
type IndexPatternState = Record;
+type LoadingState = Record;
export function IndexPatternContextProvider({ children }: ProviderProps) {
- const [loading, setLoading] = useState(false);
- const [selectedApp, setSelectedApp] = useState();
+ const [loading, setLoading] = useState({} as LoadingState);
const [indexPatterns, setIndexPatterns] = useState({} as IndexPatternState);
const [hasAppData, setHasAppData] = useState({
infra_metrics: null,
@@ -49,10 +48,9 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
const loadIndexPattern: IIndexPatternContext['loadIndexPattern'] = useCallback(
async ({ dataType }) => {
- setSelectedApp(dataType);
+ if (hasAppData[dataType] === null && !loading[dataType]) {
+ setLoading((prevState) => ({ ...prevState, [dataType]: true }));
- if (hasAppData[dataType] === null) {
- setLoading(true);
try {
let hasDataT = false;
let indices: string | undefined = '';
@@ -78,23 +76,22 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
setIndexPatterns((prevState) => ({ ...prevState, [dataType]: indPattern }));
}
- setLoading(false);
+ setLoading((prevState) => ({ ...prevState, [dataType]: false }));
} catch (e) {
- setLoading(false);
+ setLoading((prevState) => ({ ...prevState, [dataType]: false }));
}
}
},
- [data, hasAppData]
+ [data, hasAppData, loading]
);
return (
loadingT),
}}
>
{children}
@@ -102,19 +99,23 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
);
}
-export const useAppIndexPatternContext = () => {
- const { selectedApp, loading, hasAppData, loadIndexPattern, indexPatterns } = useContext(
+export const useAppIndexPatternContext = (dataType?: AppDataType) => {
+ const { loading, hasAppData, loadIndexPattern, indexPatterns } = useContext(
(IndexPatternContext as unknown) as Context
);
+ if (dataType && !indexPatterns?.[dataType] && !loading) {
+ loadIndexPattern({ dataType });
+ }
+
return useMemo(() => {
return {
hasAppData,
- selectedApp,
loading,
- indexPattern: indexPatterns?.[selectedApp],
- hasData: hasAppData?.[selectedApp],
+ indexPatterns,
+ indexPattern: dataType ? indexPatterns?.[dataType] : undefined,
+ hasData: dataType ? hasAppData?.[dataType] : undefined,
loadIndexPattern,
};
- }, [hasAppData, indexPatterns, loadIndexPattern, loading, selectedApp]);
+ }, [dataType, hasAppData, indexPatterns, loadIndexPattern, loading]);
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
index 1c85bc5089b2a..11487afe28e96 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
@@ -8,17 +8,13 @@
import { useMemo } from 'react';
import { isEmpty } from 'lodash';
import { TypedLensByValueInput } from '../../../../../../lens/public';
-import { LensAttributes } from '../configurations/lens_attributes';
+import { LayerConfig, LensAttributes } from '../configurations/lens_attributes';
import { useSeriesStorage } from './use_series_storage';
import { getDefaultConfigs } from '../configurations/default_configs';
import { DataSeries, SeriesUrl, UrlFilter } from '../types';
import { useAppIndexPatternContext } from './use_app_index_pattern';
-interface Props {
- seriesId: string;
-}
-
export const getFiltersFromDefs = (
reportDefinitions: SeriesUrl['reportDefinitions'],
dataViewConfig: DataSeries
@@ -37,54 +33,51 @@ export const getFiltersFromDefs = (
});
};
-export const useLensAttributes = ({
- seriesId,
-}: Props): TypedLensByValueInput['attributes'] | null => {
- const { getSeries } = useSeriesStorage();
- const series = getSeries(seriesId);
- const { breakdown, seriesType, operationType, reportType, dataType, reportDefinitions = {} } =
- series ?? {};
+export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => {
+ const { allSeriesIds, allSeries } = useSeriesStorage();
- const { indexPattern } = useAppIndexPatternContext();
+ const { indexPatterns } = useAppIndexPatternContext();
return useMemo(() => {
- if (!indexPattern || !reportType || isEmpty(reportDefinitions)) {
+ if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) {
return null;
}
- const dataViewConfig = getDefaultConfigs({
- reportType,
- dataType,
- indexPattern,
- });
+ const layerConfigs: LayerConfig[] = [];
+
+ allSeriesIds.forEach((seriesIdT) => {
+ const seriesT = allSeries[seriesIdT];
+ const indexPattern = indexPatterns?.[seriesT?.dataType];
+ if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) {
+ const reportViewConfig = getDefaultConfigs({
+ reportType: seriesT.reportType,
+ dataType: seriesT.dataType,
+ indexPattern,
+ });
- const filters: UrlFilter[] = (series.filters ?? []).concat(
- getFiltersFromDefs(reportDefinitions, dataViewConfig)
- );
+ const filters: UrlFilter[] = (seriesT.filters ?? []).concat(
+ getFiltersFromDefs(seriesT.reportDefinitions, reportViewConfig)
+ );
- const lensAttributes = new LensAttributes(
- indexPattern,
- dataViewConfig,
- seriesType,
- filters,
- operationType,
- reportDefinitions,
- breakdown
- );
+ layerConfigs.push({
+ filters,
+ indexPattern,
+ reportConfig: reportViewConfig,
+ breakdown: seriesT.breakdown,
+ operationType: seriesT.operationType,
+ seriesType: seriesT.seriesType,
+ reportDefinitions: seriesT.reportDefinitions ?? {},
+ time: seriesT.time,
+ });
+ }
+ });
- if (breakdown) {
- lensAttributes.addBreakdown(breakdown);
+ if (layerConfigs.length < 1) {
+ return null;
}
+ const lensAttributes = new LensAttributes(layerConfigs);
+
return lensAttributes.getJSON();
- }, [
- indexPattern,
- reportType,
- reportDefinitions,
- dataType,
- series.filters,
- seriesType,
- operationType,
- breakdown,
- ]);
+ }, [indexPatterns, allSeriesIds, allSeries]);
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
index fac75f910a93f..e9ae43950d47d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
@@ -12,7 +12,7 @@ import {
} from '../../../../../../../../src/plugins/kibana_utils/public';
import type {
AppDataType,
- ReportViewTypeId,
+ ReportViewType,
SeriesUrl,
UrlFilter,
URLReportDefinition,
@@ -36,6 +36,16 @@ interface ProviderProps {
storage: IKbnUrlStateStorage | ISessionStorageStateStorage;
}
+function convertAllShortSeries(allShortSeries: AllShortSeries) {
+ const allSeriesIds = Object.keys(allShortSeries);
+ const allSeriesN: AllSeries = {};
+ allSeriesIds.forEach((seriesKey) => {
+ allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]);
+ });
+
+ return allSeriesN;
+}
+
export function UrlStorageContextProvider({
children,
storage,
@@ -45,15 +55,14 @@ export function UrlStorageContextProvider({
const [allShortSeries, setAllShortSeries] = useState(
() => storage.get(allSeriesKey) ?? {}
);
- const [allSeries, setAllSeries] = useState({});
+ const [allSeries, setAllSeries] = useState(() =>
+ convertAllShortSeries(storage.get(allSeriesKey) ?? {})
+ );
const [firstSeriesId, setFirstSeriesId] = useState('');
useEffect(() => {
const allSeriesIds = Object.keys(allShortSeries);
- const allSeriesN: AllSeries = {};
- allSeriesIds.forEach((seriesKey) => {
- allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]);
- });
+ const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {});
setAllSeries(allSeriesN);
setFirstSeriesId(allSeriesIds?.[0]);
@@ -68,8 +77,10 @@ export function UrlStorageContextProvider({
};
const removeSeries = (seriesIdN: string) => {
- delete allShortSeries[seriesIdN];
- delete allSeries[seriesIdN];
+ setAllShortSeries((prevState) => {
+ delete prevState[seriesIdN];
+ return { ...prevState };
+ });
};
const allSeriesIds = Object.keys(allShortSeries);
@@ -115,7 +126,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
interface ShortUrlSeries {
[URL_KEYS.OPERATION_TYPE]?: OperationType;
- [URL_KEYS.REPORT_TYPE]?: ReportViewTypeId;
+ [URL_KEYS.REPORT_TYPE]?: ReportViewType;
[URL_KEYS.DATA_TYPE]?: AppDataType;
[URL_KEYS.SERIES_TYPE]?: SeriesType;
[URL_KEYS.BREAK_DOWN]?: string;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
index 3de29b02853e8..e55752ceb62ba 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
@@ -25,9 +25,11 @@ import { TypedLensByValueInput } from '../../../../../lens/public';
export function ExploratoryViewPage({
saveAttributes,
+ multiSeries = false,
useSessionStorage = false,
}: {
useSessionStorage?: boolean;
+ multiSeries?: boolean;
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
}) {
useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' });
@@ -59,7 +61,7 @@ export function ExploratoryViewPage({
-
+
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
index 8e54ab7629d26..972e3beb4b722 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
@@ -35,8 +35,11 @@ import { getStubIndexPattern } from '../../../../../../../src/plugins/data/publi
import indexPatternData from './configurations/test_data/test_index_pattern.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services';
-import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
-import { UrlFilter } from './types';
+import {
+ IndexPattern,
+ IndexPatternsContract,
+} from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
+import { AppDataType, UrlFilter } from './types';
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
import { ListItem } from '../../../hooks/use_values_list';
@@ -232,11 +235,11 @@ export const mockAppIndexPattern = () => {
const loadIndexPattern = jest.fn();
const spy = jest.spyOn(useAppIndexPatternHook, 'useAppIndexPatternContext').mockReturnValue({
indexPattern: mockIndexPattern,
- selectedApp: 'ux',
hasData: true,
loading: false,
hasAppData: { ux: true } as any,
loadIndexPattern,
+ indexPatterns: ({ ux: mockIndexPattern } as unknown) as Record,
});
return { spy, loadIndexPattern };
};
@@ -260,7 +263,7 @@ function mockSeriesStorageContext({
}) {
const mockDataSeries = data || {
'performance-distribution': {
- reportType: 'dist',
+ reportType: 'data-distribution',
dataType: 'ux',
breakdown: breakdown || 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
index 9ae8b68bf3e8c..50c2f91e6067d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
@@ -27,18 +27,14 @@ export function SeriesChartTypesSelect({
seriesTypes?: SeriesType[];
defaultChartType: SeriesType;
}) {
- const { getSeries, setSeries, allSeries } = useSeriesStorage();
+ const { getSeries, setSeries } = useSeriesStorage();
const series = getSeries(seriesId);
const seriesType = series?.seriesType ?? defaultChartType;
const onChange = (value: SeriesType) => {
- Object.keys(allSeries).forEach((seriesKey) => {
- const seriesN = allSeries[seriesKey];
-
- setSeries(seriesKey, { ...seriesN, seriesType: value });
- });
+ setSeries(seriesId, { ...series, seriesType: value });
};
return (
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
index e3c1666c533ef..b10702ebded57 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
@@ -29,7 +29,14 @@ describe('DataTypesCol', function () {
fireEvent.click(screen.getByText(/user experience \(rum\)/i));
expect(setSeries).toHaveBeenCalledTimes(1);
- expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux' });
+ expect(setSeries).toHaveBeenCalledWith(seriesId, {
+ dataType: 'ux',
+ isNew: true,
+ time: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ });
});
it('should set series on change on already selected', function () {
@@ -37,7 +44,7 @@ describe('DataTypesCol', function () {
data: {
[seriesId]: {
dataType: 'synthetics' as const,
- reportType: 'kpi' as const,
+ reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
index 985afdf888868..f386f62d9ed73 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
@@ -31,7 +31,11 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) {
if (!dataType) {
removeSeries(seriesId);
} else {
- setSeries(seriesId || `${dataType}-series`, { dataType } as any);
+ setSeries(seriesId || `${dataType}-series`, {
+ dataType,
+ isNew: true,
+ time: series.time,
+ } as any);
}
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
index 175fbea9445c1..6be78084ae195 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
@@ -8,14 +8,23 @@
import React from 'react';
import styled from 'styled-components';
import { SeriesDatePicker } from '../../series_date_picker';
+import { DateRangePicker } from '../../series_date_picker/date_range_picker';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
interface Props {
seriesId: string;
}
export function DatePickerCol({ seriesId }: Props) {
+ const { firstSeriesId, getSeries } = useSeriesStorage();
+ const { reportType } = getSeries(firstSeriesId);
+
return (
-
+ {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
index c262a94f968be..516f04e3812ba 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
@@ -22,7 +22,7 @@ describe('OperationTypeSelect', function () {
data: {
'performance-distribution': {
dataType: 'ux' as const,
- reportType: 'kpi' as const,
+ reportType: 'kpi-over-time' as const,
operationType: 'median' as const,
time: { from: 'now-15m', to: 'now' },
},
@@ -39,7 +39,7 @@ describe('OperationTypeSelect', function () {
data: {
'series-id': {
dataType: 'ux' as const,
- reportType: 'kpi' as const,
+ reportType: 'kpi-over-time' as const,
operationType: 'median' as const,
time: { from: 'now-15m', to: 'now' },
},
@@ -53,7 +53,7 @@ describe('OperationTypeSelect', function () {
expect(setSeries).toHaveBeenCalledWith('series-id', {
operationType: 'median',
dataType: 'ux',
- reportType: 'kpi',
+ reportType: 'kpi-over-time',
time: { from: 'now-15m', to: 'now' },
});
@@ -61,7 +61,7 @@ describe('OperationTypeSelect', function () {
expect(setSeries).toHaveBeenCalledWith('series-id', {
operationType: '95th',
dataType: 'ux',
- reportType: 'kpi',
+ reportType: 'kpi-over-time',
time: { from: 'now-15m', to: 'now' },
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
index 805186e877d57..203382afc1624 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
@@ -15,7 +15,7 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel
describe('Series Builder ReportBreakdowns', function () {
const seriesId = 'test-series-id';
const dataViewSeries = getDefaultConfigs({
- reportType: 'dist',
+ reportType: 'data-distribution',
dataType: 'ux',
indexPattern: mockIndexPattern,
});
@@ -45,7 +45,7 @@ describe('Series Builder ReportBreakdowns', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
breakdown: USER_AGENT_OS,
dataType: 'ux',
- reportType: 'dist',
+ reportType: 'data-distribution',
time: { from: 'now-15m', to: 'now' },
});
});
@@ -67,7 +67,7 @@ describe('Series Builder ReportBreakdowns', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
breakdown: undefined,
dataType: 'ux',
- reportType: 'dist',
+ reportType: 'data-distribution',
time: { from: 'now-15m', to: 'now' },
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
index e947961fb4300..2e5c674b9fad8 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
@@ -22,7 +22,7 @@ describe('Series Builder ReportDefinitionCol', function () {
const seriesId = 'test-series-id';
const dataViewSeries = getDefaultConfigs({
- reportType: 'dist',
+ reportType: 'data-distribution',
indexPattern: mockIndexPattern,
dataType: 'ux',
});
@@ -31,7 +31,7 @@ describe('Series Builder ReportDefinitionCol', function () {
data: {
[seriesId]: {
dataType: 'ux' as const,
- reportType: 'dist' as const,
+ reportType: 'data-distribution' as const,
time: { from: 'now-30d', to: 'now' },
reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
},
@@ -81,7 +81,7 @@ describe('Series Builder ReportDefinitionCol', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
dataType: 'ux',
reportDefinitions: {},
- reportType: 'dist',
+ reportType: 'data-distribution',
time: { from: 'now-30d', to: 'now' },
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
index 338f5d52c26fa..47962af0d4bc4 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import styled from 'styled-components';
-import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { CustomReportField } from '../custom_report_field';
import { DataSeries, URLReportDefinition } from '../../types';
@@ -36,8 +35,6 @@ export function ReportDefinitionCol({
dataViewSeries: DataSeries;
seriesId: string;
}) {
- const { indexPattern } = useAppIndexPatternContext();
-
const { getSeries, setSeries } = useSeriesStorage();
const series = getSeries(seriesId);
@@ -69,21 +66,20 @@ export function ReportDefinitionCol({
- {indexPattern &&
- reportDefinitions.map(({ field, custom, options }) => (
-
- {!custom ? (
-
- ) : (
-
- )}
-
- ))}
+ {reportDefinitions.map(({ field, custom, options }) => (
+
+ {!custom ? (
+
+ ) : (
+
+ )}
+
+ ))}
{(hasOperationType || columnType === 'operation') && (
{
- if (!custom && selectedReportDefinitions?.[fieldT] && fieldT !== field) {
+ if (!custom && indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) {
const values = selectedReportDefinitions?.[fieldT];
const valueFilter = buildPhrasesFilter(fieldT, values, indexPattern)[0];
filtersN.push(valueFilter.query);
@@ -64,16 +64,18 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }:
return (
- onChange(field, val)}
- filters={queryFilters}
- time={series.time}
- fullWidth={true}
- />
+ {indexPattern && (
+ onChange(field, val)}
+ filters={queryFilters}
+ time={series.time}
+ fullWidth={true}
+ />
+ )}
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
index 7ca947fed0bc9..f35639388aac5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
@@ -15,7 +15,7 @@ describe('Series Builder ReportFilters', function () {
const seriesId = 'test-series-id';
const dataViewSeries = getDefaultConfigs({
- reportType: 'dist',
+ reportType: 'data-distribution',
indexPattern: mockIndexPattern,
dataType: 'ux',
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
index f36d64ca5bbbd..f7cfe06c0d928 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
@@ -11,10 +11,9 @@ import { mockAppIndexPattern, render } from '../../rtl_helpers';
import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col';
import { ReportTypes } from '../series_builder';
import { DEFAULT_TIME } from '../../configurations/constants';
-import { NEW_SERIES_KEY } from '../../hooks/use_series_storage';
describe('ReportTypesCol', function () {
- const seriesId = 'test-series-id';
+ const seriesId = 'performance-distribution';
mockAppIndexPattern();
@@ -40,7 +39,7 @@ describe('ReportTypesCol', function () {
breakdown: 'user_agent.name',
dataType: 'ux',
reportDefinitions: {},
- reportType: 'kpi',
+ reportType: 'kpi-over-time',
time: { from: 'now-15m', to: 'now' },
});
expect(setSeries).toHaveBeenCalledTimes(1);
@@ -49,11 +48,12 @@ describe('ReportTypesCol', function () {
it('should set selected as filled', function () {
const initSeries = {
data: {
- [NEW_SERIES_KEY]: {
+ [seriesId]: {
dataType: 'synthetics' as const,
- reportType: 'kpi' as const,
+ reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
+ isNew: true,
},
},
};
@@ -74,6 +74,7 @@ describe('ReportTypesCol', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
dataType: 'synthetics',
time: DEFAULT_TIME,
+ isNew: true,
});
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
index 9fff8dae14a47..64c7b48c668b8 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
@@ -7,27 +7,33 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
+import { map } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import styled from 'styled-components';
-import { ReportViewTypeId, SeriesUrl } from '../../types';
+import { ReportViewType, SeriesUrl } from '../../types';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { DEFAULT_TIME } from '../../configurations/constants';
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
+import { ReportTypeItem, SELECT_DATA_TYPE } from '../series_builder';
interface Props {
seriesId: string;
- reportTypes: Array<{ id: ReportViewTypeId; label: string }>;
+ reportTypes: ReportTypeItem[];
}
export function ReportTypesCol({ seriesId, reportTypes }: Props) {
- const { setSeries, getSeries } = useSeriesStorage();
+ const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage();
const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId);
- const { loading, hasData, selectedApp } = useAppIndexPatternContext();
+ const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType);
- if (!loading && !hasData && selectedApp) {
+ if (!restSeries.dataType) {
+ return {SELECT_DATA_TYPE} ;
+ }
+
+ if (!loading && !hasData) {
return (
firstSeriesId !== seriesId && reportType !== firstSeries.reportType
+ ),
+ 'reportType'
+ );
+
return reportTypes?.length > 0 ? (
- {reportTypes.map(({ id: reportType, label }) => (
+ {reportTypes.map(({ reportType, label }) => (
{
if (reportType === selectedReportType) {
setSeries(seriesId, {
dataType: restSeries.dataType,
time: DEFAULT_TIME,
+ isNew: true,
} as SeriesUrl);
} else {
setSeries(seriesId, {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx
new file mode 100644
index 0000000000000..874171de123d2
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+import { EuiIcon, EuiText } from '@elastic/eui';
+import moment from 'moment';
+
+interface Props {
+ lastUpdated?: number;
+}
+export function LastUpdated({ lastUpdated }: Props) {
+ const [refresh, setRefresh] = useState(() => Date.now());
+
+ useEffect(() => {
+ const interVal = setInterval(() => {
+ setRefresh(Date.now());
+ }, 1000);
+
+ return () => {
+ clearInterval(interVal);
+ };
+ }, []);
+
+ if (!lastUpdated) {
+ return null;
+ }
+
+ return (
+
+ Last Updated: {moment(lastUpdated).from(refresh)}
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
index 9aef16931d7ec..e596eb6be354a 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
@@ -5,11 +5,19 @@
* 2.0.
*/
-import React, { RefObject } from 'react';
-
+import React, { RefObject, useEffect, useState } from 'react';
+import { isEmpty } from 'lodash';
import { i18n } from '@kbn/i18n';
-import { EuiBasicTable } from '@elastic/eui';
-import { AppDataType, ReportViewTypeId, ReportViewTypes, SeriesUrl } from '../types';
+import {
+ EuiBasicTable,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiSwitch,
+} from '@elastic/eui';
+import { rgba } from 'polished';
+import { AppDataType, DataSeries, ReportViewType, SeriesUrl } from '../types';
import { DataTypesCol } from './columns/data_types_col';
import { ReportTypesCol } from './columns/report_types_col';
import { ReportDefinitionCol } from './columns/report_definition_col';
@@ -18,6 +26,10 @@ import { ReportBreakdowns } from './columns/report_breakdowns';
import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
import { getDefaultConfigs } from '../configurations/default_configs';
+import { SeriesEditor } from '../series_editor/series_editor';
+import { SeriesActions } from '../series_editor/columns/series_actions';
+import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
+import { LastUpdated } from './last_updated';
import {
CORE_WEB_VITALS_LABEL,
DEVICE_DISTRIBUTION_LABEL,
@@ -25,72 +37,94 @@ import {
PERF_DIST_LABEL,
} from '../configurations/constants/labels';
-export const ReportTypes: Record> = {
+export interface ReportTypeItem {
+ id: string;
+ reportType: ReportViewType;
+ label: string;
+}
+
+export const ReportTypes: Record = {
synthetics: [
- { id: 'kpi', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', label: PERF_DIST_LABEL },
+ { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
+ { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
],
ux: [
- { id: 'kpi', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', label: PERF_DIST_LABEL },
- { id: 'cwv', label: CORE_WEB_VITALS_LABEL },
+ { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
+ { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
+ { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL },
],
mobile: [
- { id: 'kpi', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', label: PERF_DIST_LABEL },
- { id: 'mdd', label: DEVICE_DISTRIBUTION_LABEL },
+ { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
+ { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
+ { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL },
],
apm: [],
infra_logs: [],
infra_metrics: [],
};
+interface BuilderItem {
+ id: string;
+ series: SeriesUrl;
+ seriesConfig?: DataSeries;
+}
+
export function SeriesBuilder({
seriesBuilderRef,
- seriesId,
+ lastUpdated,
+ multiSeries,
}: {
- seriesId: string;
seriesBuilderRef: RefObject;
+ lastUpdated?: number;
+ multiSeries?: boolean;
}) {
- const { getSeries, setSeries, removeSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
- const {
- dataType,
- seriesType,
- reportType,
- reportDefinitions = {},
- filters = [],
- operationType,
- breakdown,
- time,
- } = series;
-
- const { indexPattern, loading, hasData } = useAppIndexPatternContext();
-
- const getDataViewSeries = () => {
- return getDefaultConfigs({
- dataType,
- indexPattern,
- reportType: reportType!,
- });
- };
+ const [editorItems, setEditorItems] = useState([]);
+ const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage();
+
+ const { loading, indexPatterns } = useAppIndexPatternContext();
+
+ useEffect(() => {
+ const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => {
+ if (indexPatterns?.[dataType]) {
+ return getDefaultConfigs({
+ dataType,
+ indexPattern: indexPatterns[dataType],
+ reportType: reportType!,
+ });
+ }
+ };
+
+ const seriesToEdit: BuilderItem[] =
+ allSeriesIds
+ .filter((sId) => {
+ return allSeries?.[sId]?.isNew;
+ })
+ .map((sId) => {
+ const series = getSeries(sId);
+ const seriesConfig = getDataViewSeries(series.dataType, series.reportType);
+
+ return { id: sId, series, seriesConfig };
+ }) ?? [];
+ const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }];
+ setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries);
+ }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]);
const columns = [
{
name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', {
defaultMessage: 'Data Type',
}),
+ field: 'id',
width: '15%',
- render: (val: string) => ,
+ render: (seriesId: string) => ,
},
{
name: i18n.translate('xpack.observability.expView.seriesBuilder.report', {
defaultMessage: 'Report',
}),
width: '15%',
- render: (val: string) => (
+ field: 'id',
+ render: (seriesId: string, { series: { dataType } }: BuilderItem) => (
),
},
@@ -99,12 +133,16 @@ export function SeriesBuilder({
defaultMessage: 'Definition',
}),
width: '30%',
- render: (val: string) => {
- if (dataType && hasData) {
+ field: 'id',
+ render: (
+ seriesId: string,
+ { series: { dataType, reportType }, seriesConfig }: BuilderItem
+ ) => {
+ if (dataType && seriesConfig) {
return loading ? (
LOADING_VIEW
) : reportType ? (
-
+
) : (
SELECT_REPORT_TYPE
);
@@ -118,9 +156,10 @@ export function SeriesBuilder({
defaultMessage: 'Filters',
}),
width: '20%',
- render: (val: string) =>
- reportType && indexPattern ? (
-
+ field: 'id',
+ render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
+ reportType && seriesConfig ? (
+
) : null,
},
{
@@ -129,53 +168,126 @@ export function SeriesBuilder({
}),
width: '20%',
field: 'id',
- render: (val: string) =>
- reportType && indexPattern ? (
-
+ render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
+ reportType && seriesConfig ? (
+
) : null,
},
+ ...(multiSeries
+ ? [
+ {
+ name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', {
+ defaultMessage: 'Actions',
+ }),
+ align: 'center' as const,
+ width: '10%',
+ field: 'id',
+ render: (seriesId: string, item: BuilderItem) => (
+
+ ),
+ },
+ ]
+ : []),
];
- // TODO: Remove this if remain unused during multiple series view
- // @ts-expect-error
- const addSeries = () => {
- if (reportType) {
- const newSeriesId = `${
- reportDefinitions?.['service.name'] ||
- reportDefinitions?.['monitor.id'] ||
- ReportViewTypes[reportType]
- }`;
-
- const newSeriesN: SeriesUrl = {
- dataType,
- time,
- filters,
- breakdown,
- reportType,
- seriesType,
- operationType,
- reportDefinitions,
- };
-
- setSeries(newSeriesId, newSeriesN);
- removeSeries(NEW_SERIES_KEY);
- }
+ const applySeries = () => {
+ editorItems.forEach(({ series, id: seriesId }) => {
+ const { reportType, reportDefinitions, isNew, ...restSeries } = series;
+
+ if (reportType && !isEmpty(reportDefinitions)) {
+ const reportDefId = Object.values(reportDefinitions ?? {})[0];
+ const newSeriesId = `${reportDefId}-${reportType}`;
+
+ const newSeriesN: SeriesUrl = {
+ ...restSeries,
+ reportType,
+ reportDefinitions,
+ };
+
+ setSeries(newSeriesId, newSeriesN);
+ removeSeries(seriesId);
+ }
+ });
};
- const items = [{ id: seriesId }];
+ const addSeries = () => {
+ const prevSeries = allSeries?.[allSeriesIds?.[0]];
+ setSeries(
+ `${NEW_SERIES_KEY}-${editorItems.length + 1}`,
+ prevSeries
+ ? ({ isNew: true, time: prevSeries.time } as SeriesUrl)
+ : ({ isNew: true } as SeriesUrl)
+ );
+ };
return (
-
-
-
+
+ {multiSeries && (
+
+
+
+
+
+ {}}
+ compressed
+ />
+
+
+ applySeries()} isDisabled={true} size="s">
+ {i18n.translate('xpack.observability.expView.seriesBuilder.apply', {
+ defaultMessage: 'Apply changes',
+ })}
+
+
+
+ addSeries()} size="s">
+ {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', {
+ defaultMessage: 'Add Series',
+ })}
+
+
+
+ )}
+
+ {multiSeries && }
+ {editorItems.length > 0 && (
+
+ )}
+
+
+
);
}
+const Wrapper = euiStyled.div`
+ max-height: 50vh;
+ overflow-y: scroll;
+ overflow-x: clip;
+ &::-webkit-scrollbar {
+ height: ${({ theme }) => theme.eui.euiScrollBar};
+ width: ${({ theme }) => theme.eui.euiScrollBar};
+ }
+ &::-webkit-scrollbar-thumb {
+ background-clip: content-box;
+ background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
+ border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
+ }
+ &::-webkit-scrollbar-corner,
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+`;
+
export const LOADING_VIEW = i18n.translate(
'xpack.observability.expView.seriesBuilder.loadingView',
{
@@ -189,3 +301,10 @@ export const SELECT_REPORT_TYPE = i18n.translate(
defaultMessage: 'No report type selected',
}
);
+
+export const SELECT_DATA_TYPE = i18n.translate(
+ 'xpack.observability.expView.seriesBuilder.selectDataType',
+ {
+ defaultMessage: 'No data type selected',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx
new file mode 100644
index 0000000000000..c30863585b3b0
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui';
+import DateMath from '@elastic/datemath';
+import { Moment } from 'moment';
+import { useSeriesStorage } from '../hooks/use_series_storage';
+import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public';
+
+export const parseAbsoluteDate = (date: string, options = {}) => {
+ return DateMath.parse(date, options)!;
+};
+export function DateRangePicker({ seriesId }: { seriesId: string }) {
+ const { firstSeriesId, getSeries, setSeries } = useSeriesStorage();
+ const dateFormat = useUiSetting('dateFormat');
+
+ const {
+ time: { from, to },
+ reportType,
+ } = getSeries(firstSeriesId);
+
+ const series = getSeries(seriesId);
+
+ const {
+ time: { from: seriesFrom, to: seriesTo },
+ } = series;
+
+ const startDate = parseAbsoluteDate(seriesFrom ?? from)!;
+ const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!;
+
+ const onStartChange = (newDate: Moment) => {
+ if (reportType === 'kpi-over-time') {
+ const mainStartDate = parseAbsoluteDate(from)!;
+ const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!;
+ const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond');
+ const newFrom = newDate.toISOString();
+ const newTo = newDate.add(totalDuration, 'millisecond').toISOString();
+
+ setSeries(seriesId, {
+ ...series,
+ time: { from: newFrom, to: newTo },
+ });
+ } else {
+ const newFrom = newDate.toISOString();
+
+ setSeries(seriesId, {
+ ...series,
+ time: { from: newFrom, to: seriesTo },
+ });
+ }
+ };
+ const onEndChange = (newDate: Moment) => {
+ if (reportType === 'kpi-over-time') {
+ const mainStartDate = parseAbsoluteDate(from)!;
+ const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!;
+ const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond');
+ const newTo = newDate.toISOString();
+ const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString();
+
+ setSeries(seriesId, {
+ ...series,
+ time: { from: newFrom, to: newTo },
+ });
+ } else {
+ const newTo = newDate.toISOString();
+
+ setSeries(seriesId, {
+ ...series,
+ time: { from: seriesFrom, to: newTo },
+ });
+ }
+ };
+
+ return (
+ endDate}
+ aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', {
+ defaultMessage: 'Start date',
+ })}
+ dateFormat={dateFormat}
+ showTimeSelect
+ />
+ }
+ endDateControl={
+ endDate}
+ aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', {
+ defaultMessage: 'End date',
+ })}
+ dateFormat={dateFormat}
+ showTimeSelect
+ />
+ }
+ />
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
index d6a70532f4257..e21da424b58c8 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
@@ -43,7 +43,7 @@ export function SeriesDatePicker({ seriesId }: Props) {
if (!series || !series.time) {
setSeries(seriesId, { ...series, time: DEFAULT_TIME });
}
- }, [seriesId, series, setSeries]);
+ }, [series, seriesId, setSeries]);
return (
-
+ {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
index a78f6adeca39f..0f0cec0fbfcff 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
@@ -41,8 +41,6 @@ export function FilterExpanded({
isNegated,
filters: defaultFilters,
}: Props) {
- const { indexPattern } = useAppIndexPatternContext();
-
const [value, setValue] = useState('');
const [isOpen, setIsOpen] = useState({ value: '', negate: false });
@@ -53,23 +51,25 @@ export function FilterExpanded({
const queryFilters: ESFilter[] = [];
+ const { indexPatterns } = useAppIndexPatternContext(series.dataType);
+
defaultFilters?.forEach((qFilter: PersistableFilter | ExistsFilter) => {
if (qFilter.query) {
queryFilters.push(qFilter.query);
}
const asExistFilter = qFilter as ExistsFilter;
if (asExistFilter?.exists) {
- queryFilters.push(asExistFilter.exists as QueryDslQueryContainer);
+ queryFilters.push({ exists: asExistFilter.exists } as QueryDslQueryContainer);
}
});
const { values, loading } = useValuesList({
query: value,
- indexPatternTitle: indexPattern?.title,
sourceField: field,
time: series.time,
keepHistory: true,
filters: queryFilters,
+ indexPatternTitle: indexPatterns[series.dataType]?.title,
});
const filters = series?.filters ?? [];
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
index 79eb858b7624b..c1790fea8c0c4 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
@@ -139,7 +139,7 @@ describe('FilterValueButton', function () {
/>
);
- expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toBeCalledWith(
expect.objectContaining({
filters: [
@@ -170,7 +170,7 @@ describe('FilterValueButton', function () {
/>
);
- expect(spy).toHaveBeenCalledTimes(2);
+ expect(spy).toHaveBeenCalledTimes(6);
expect(spy).toBeCalledWith(
expect.objectContaining({
filters: [
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
index f04295a90e475..bf4ca6eb83d94 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
@@ -41,7 +41,7 @@ export function FilterValueButton({
const series = getSeries(seriesId);
- const { indexPattern } = useAppIndexPatternContext();
+ const { indexPatterns } = useAppIndexPatternContext(series.dataType);
const { setFilter, removeFilter } = useSeriesFilters({ seriesId });
@@ -96,7 +96,6 @@ export function FilterValueButton({
) : (
button
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx
index dc84352ff3b3d..e75f308dab1e5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx
@@ -26,9 +26,9 @@ export function RemoveSeries({ seriesId }: Props) {
defaultMessage: 'Click to remove series',
})}
iconType="cross"
- color="primary"
+ color="danger"
onClick={onClick}
- size="m"
+ size="s"
/>
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
index 086a1d4341bbc..51ebe6c6bd9d5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
@@ -8,33 +8,93 @@
import React from 'react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { isEmpty } from 'lodash';
import { RemoveSeries } from './remove_series';
-import { NEW_SERIES_KEY, useSeriesStorage } from '../../hooks/use_series_storage';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { SeriesUrl } from '../../types';
interface Props {
seriesId: string;
+ editorMode?: boolean;
}
-export function SeriesActions({ seriesId }: Props) {
- const { getSeries, removeSeries, setSeries } = useSeriesStorage();
+export function SeriesActions({ seriesId, editorMode = false }: Props) {
+ const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage();
const series = getSeries(seriesId);
const onEdit = () => {
- removeSeries(seriesId);
- setSeries(NEW_SERIES_KEY, { ...series });
+ setSeries(seriesId, { ...series, isNew: true });
+ };
+
+ const copySeries = () => {
+ let copySeriesId: string = `${seriesId}-copy`;
+ if (allSeriesIds.includes(copySeriesId)) {
+ copySeriesId = copySeriesId + allSeriesIds.length;
+ }
+ setSeries(copySeriesId, series);
+ };
+
+ const { reportType, reportDefinitions, isNew, ...restSeries } = series;
+ const isSaveAble = reportType && !isEmpty(reportDefinitions);
+
+ const saveSeries = () => {
+ if (isSaveAble) {
+ const reportDefId = Object.values(reportDefinitions ?? {})[0];
+ let newSeriesId = `${reportDefId}-${reportType}`;
+
+ if (allSeriesIds.includes(newSeriesId)) {
+ newSeriesId = `${newSeriesId}-${allSeriesIds.length}`;
+ }
+ const newSeriesN: SeriesUrl = {
+ ...restSeries,
+ reportType,
+ reportDefinitions,
+ };
+
+ setSeries(newSeriesId, newSeriesN);
+ removeSeries(seriesId);
+ }
};
return (
-
-
-
-
+
+ {!editorMode && (
+
+
+
+ )}
+ {editorMode && (
+
+
+
+ )}
+ {editorMode && (
+
+
+
+ )}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
index 8363b6b0eadfd..61081e7cc6f46 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
@@ -16,7 +16,7 @@ describe('SelectedFilters', function () {
mockAppIndexPattern();
const dataViewSeries = getDefaultConfigs({
- reportType: 'dist',
+ reportType: 'data-distribution',
indexPattern: mockIndexPattern,
dataType: 'ux',
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
index 63abb581c9c72..33496e617a3a6 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
@@ -39,7 +39,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
const { removeFilter } = useSeriesFilters({ seriesId });
- const { indexPattern } = useAppIndexPatternContext();
+ const { indexPattern } = useAppIndexPatternContext(series.dataType);
return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? (
@@ -55,6 +55,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
value={val}
removeFilter={() => removeFilter({ field, value: val, negate: false })}
negate={false}
+ indexPattern={indexPattern}
/>
))}
@@ -67,6 +68,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
value={val}
negate={true}
removeFilter={() => removeFilter({ field, value: val, negate: true })}
+ indexPattern={indexPattern}
/>
))}
@@ -87,6 +89,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
}}
negate={false}
definitionFilter={true}
+ indexPattern={indexPattern}
/>
))}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
index 17d4356dcf65b..bcceeb204a31e 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
@@ -24,7 +24,7 @@ interface EditItem {
}
export function SeriesEditor() {
- const { allSeries, firstSeriesId } = useSeriesStorage();
+ const { allSeries, allSeriesIds } = useSeriesStorage();
const columns = [
{
@@ -33,80 +33,77 @@ export function SeriesEditor() {
}),
field: 'id',
width: '15%',
- render: (val: string) => (
+ render: (seriesId: string) => (
{' '}
- {val === NEW_SERIES_KEY ? 'series-preview' : val}
+ {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId}
),
},
- ...(firstSeriesId !== NEW_SERIES_KEY
- ? [
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.filters', {
- defaultMessage: 'Filters',
- }),
- field: 'defaultFilters',
- width: '15%',
- render: (defaultFilters: string[], { id, seriesConfig }: EditItem) => (
-
- ),
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', {
- defaultMessage: 'Breakdowns',
- }),
- field: 'breakdowns',
- width: '25%',
- render: (val: string[], item: EditItem) => (
-
- ),
- },
- {
- name: (
-
-
-
- ),
- width: '20%',
- field: 'id',
- align: 'right' as const,
- render: (val: string, item: EditItem) => ,
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
- defaultMessage: 'Actions',
- }),
- align: 'center' as const,
- width: '10%',
- field: 'id',
- render: (val: string, item: EditItem) => ,
- },
- ]
- : []),
+ {
+ name: i18n.translate('xpack.observability.expView.seriesEditor.filters', {
+ defaultMessage: 'Filters',
+ }),
+ field: 'defaultFilters',
+ width: '15%',
+ render: (seriesId: string, { seriesConfig, id }: EditItem) => (
+
+ ),
+ },
+ {
+ name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', {
+ defaultMessage: 'Breakdowns',
+ }),
+ field: 'id',
+ width: '25%',
+ render: (seriesId: string, { seriesConfig, id }: EditItem) => (
+
+ ),
+ },
+ {
+ name: (
+
+
+
+ ),
+ width: '20%',
+ field: 'id',
+ align: 'right' as const,
+ render: (seriesId: string, item: EditItem) => ,
+ },
+ {
+ name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
+ defaultMessage: 'Actions',
+ }),
+ align: 'center' as const,
+ width: '10%',
+ field: 'id',
+ render: (seriesId: string, item: EditItem) => ,
+ },
];
- const allSeriesKeys = Object.keys(allSeries);
-
+ const { indexPatterns } = useAppIndexPatternContext();
const items: EditItem[] = [];
- const { indexPattern } = useAppIndexPatternContext();
-
- allSeriesKeys.forEach((seriesKey) => {
+ allSeriesIds.forEach((seriesKey) => {
const series = allSeries[seriesKey];
- if (series.reportType && indexPattern) {
+ if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) {
items.push({
id: seriesKey,
seriesConfig: getDefaultConfigs({
- indexPattern,
+ indexPattern: indexPatterns[series.dataType],
reportType: series.reportType,
dataType: series.dataType,
}),
@@ -114,6 +111,10 @@ export function SeriesEditor() {
}
});
+ if (items.length === 0 && allSeriesIds.length > 0) {
+ return null;
+ }
+
return (
<>
@@ -121,8 +122,7 @@ export function SeriesEditor() {
items={items}
rowHeader="firstName"
columns={columns}
- rowProps={() => (firstSeriesId === NEW_SERIES_KEY ? {} : { height: 100 })}
- noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.notFound', {
+ noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.seriesNotFound', {
defaultMessage: 'No series found, please add a series.',
})}
cellProps={{
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
index 73b4d7794dd51..e8fccc5baab34 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
@@ -23,7 +23,7 @@ export const ReportViewTypes = {
dist: 'data-distribution',
kpi: 'kpi-over-time',
cwv: 'core-web-vitals',
- mdd: 'mobile-device-distribution',
+ mdd: 'device-data-distribution',
} as const;
type ValueOf = T[keyof T];
@@ -56,7 +56,6 @@ export interface DataSeries {
reportType: ReportViewType;
xAxisColumn: Partial | Partial;
yAxisColumns: Array>;
-
breakdowns: string[];
defaultSeriesType: SeriesType;
defaultFilters: Array;
@@ -80,10 +79,11 @@ export interface SeriesUrl {
breakdown?: string;
filters?: UrlFilter[];
seriesType?: SeriesType;
- reportType: ReportViewTypeId;
+ reportType: ReportViewType;
operationType?: OperationType;
dataType: AppDataType;
reportDefinitions?: URLReportDefinition;
+ isNew?: boolean;
}
export interface UrlFilter {
@@ -94,6 +94,7 @@ export interface UrlFilter {
export interface ConfigProps {
indexPattern: IIndexPattern;
+ series?: SeriesUrl;
}
export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm' | 'mobile';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts
new file mode 100644
index 0000000000000..fe545fff5498d
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts
@@ -0,0 +1,148 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { urlFiltersToKueryString } from './stringify_kueries';
+import { UrlFilter } from '../types';
+import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames';
+
+describe('stringifyKueries', () => {
+ let filters: UrlFilter[];
+ beforeEach(() => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Chrome', 'Firefox'],
+ notValues: [],
+ },
+ ];
+ });
+
+ it('stringifies the current values', () => {
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Chrome\\" or \\"Firefox\\")"`
+ );
+ });
+
+ it('correctly stringifies a single value', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Chrome'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Chrome\\")"`
+ );
+ });
+
+ it('returns an empty string for an empty array', () => {
+ expect(urlFiltersToKueryString([])).toMatchInlineSnapshot(`""`);
+ });
+
+ it('returns an empty string for an empty value', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: [],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(`""`);
+ });
+
+ it('adds quotations if the value contains a space', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Google Chrome'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Google Chrome\\")"`
+ );
+ });
+
+ it('adds quotations inside parens if there are values containing spaces', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Google Chrome'],
+ notValues: ['Apple Safari'],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Google Chrome\\") and not (user_agent.name: (\\"Apple Safari\\"))"`
+ );
+ });
+
+ it('handles parens for values with greater than 2 items', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Chrome', 'Firefox', 'Safari', 'Opera'],
+ notValues: ['Safari'],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Chrome\\" or \\"Firefox\\" or \\"Safari\\" or \\"Opera\\") and not (user_agent.name: (\\"Safari\\"))"`
+ );
+ });
+
+ it('handles colon characters in values', () => {
+ filters = [
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
+ );
+ });
+
+ it('handles precending empty array', () => {
+ filters = [
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ {
+ field: USER_AGENT_NAME,
+ values: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
+ );
+ });
+
+ it('handles skipped empty arrays', () => {
+ filters = [
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ {
+ field: USER_AGENT_NAME,
+ values: [],
+ },
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"url: (\\"https://elastic.co\\" or \\"https://example.com\\") and url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
+ );
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts
new file mode 100644
index 0000000000000..8a92c724338ef
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { UrlFilter } from '../types';
+
+/**
+ * Extract a map's keys to an array, then map those keys to a string per key.
+ * The strings contain all of the values chosen for the given field (which is also the key value).
+ * Reduce the list of query strings to a singular string, with AND operators between.
+ */
+export const urlFiltersToKueryString = (urlFilters: UrlFilter[]): string => {
+ let kueryString = '';
+ urlFilters.forEach(({ field, values, notValues }) => {
+ const valuesT = values?.map((val) => `"${val}"`);
+ const notValuesT = notValues?.map((val) => `"${val}"`);
+
+ if (valuesT && valuesT?.length > 0) {
+ if (kueryString.length > 0) {
+ kueryString += ' and ';
+ }
+ kueryString += `${field}: (${valuesT.join(' or ')})`;
+ }
+
+ if (notValuesT && notValuesT?.length > 0) {
+ if (kueryString.length > 0) {
+ kueryString += ' and ';
+ }
+ kueryString += `not (${field}: (${notValuesT.join(' or ')}))`;
+ }
+ });
+
+ return kueryString;
+};
diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
index 5a7ce3502ce84..896aca79114d7 100644
--- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
+++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiSideNavItemType, ExclusiveUnion } from '@elastic/eui';
+import { EuiSideNavItemType } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
@@ -28,13 +28,9 @@ export type WrappedPageTemplateProps = Pick<
| 'pageContentProps'
| 'pageHeader'
| 'restrictWidth'
+ | 'template'
| 'isEmptyState'
-> &
- // recreate the exclusivity of bottomBar-related props
- ExclusiveUnion<
- { template?: 'default' } & Pick,
- { template: KibanaPageTemplateProps['template'] }
- >;
+>;
export interface ObservabilityPageTemplateDependencies {
currentAppId$: Observable;
diff --git a/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx b/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx
new file mode 100644
index 0000000000000..4d8779e1ea150
--- /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 f73f3b4cf57d7..442104a710601 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 78f1cb313ea9b..f93cb5c4e7919 100644
--- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx
@@ -5,44 +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: { navigateToApp },
+ application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
+ const casesUrl = getUrlForApp(CASES_APP_ID);
const userPermissions = useGetUserCasesPermissions();
const { detailName: caseId, subCaseId } = useParams<{
detailName?: string;
subCaseId?: string;
}>();
+ useReadonlyHeader();
- if (userPermissions != null && !userPermissions.read) {
- navigateToApp(`${CASES_APP_ID}`);
- 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 2986c1ff34e11..9676eb7eba147 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';
@@ -23,24 +23,27 @@ const ButtonEmpty = styled(EuiButtonEmpty)`
function ConfigureCasesPageComponent() {
const {
cases,
- application: { navigateToApp },
+ application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
+ const casesUrl = getUrlForApp(CASES_APP_ID);
const userPermissions = useGetUserCasesPermissions();
const { ObservabilityPageTemplate } = usePluginContext();
const onClickGoToCases = useCallback(
async (ev) => {
ev.preventDefault();
- return navigateToApp(`${CASES_APP_ID}`);
+ return navigateToUrl(casesUrl);
},
- [navigateToApp]
+ [casesUrl, navigateToUrl]
);
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(getCaseUrl());
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]);
- if (userPermissions != null && !userPermissions.read) {
- navigateToApp(`${CASES_APP_ID}`);
- return null;
- }
+
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, userPermissions, navigateToUrl]);
return (
{
const userPermissions = useGetUserCasesPermissions();
const { ObservabilityPageTemplate } = usePluginContext();
const {
- application: { navigateToApp },
+ application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
+ const casesUrl = getUrlForApp(CASES_APP_ID);
const goTo = useCallback(
async (ev) => {
ev.preventDefault();
- return navigateToApp(CASES_APP_ID);
+ return navigateToUrl(casesUrl);
},
- [navigateToApp]
+ [casesUrl, navigateToUrl]
);
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(getCaseUrl());
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]);
- if (userPermissions != null && !userPermissions.crud) {
- navigateToApp(`${CASES_APP_ID}`);
- return null;
- }
+
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.crud) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, navigateToUrl, userPermissions]);
return (
{
'Are 503 errors stacking up? Are services responding? Is CPU and RAM utilization jumping? See warnings as they happen—not as part of the post-mortem.',
}),
linkTitle: i18n.translate('xpack.observability.emptySection.apps.alert.link', {
- defaultMessage: 'Create alert',
+ defaultMessage: 'Create rule',
}),
href: core.http.basePath.prepend(
'/app/management/insightsAndAlerting/triggersActions/alerts'
diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx
index 92f51aeff9bd6..f97e3fb996441 100644
--- a/x-pack/plugins/observability/public/routes/index.tsx
+++ b/x-pack/plugins/observability/public/routes/index.tsx
@@ -112,4 +112,18 @@ export const routes = {
}),
},
},
+ // enable this to test multi series architecture
+ // '/exploratory-view/multi': {
+ // handler: () => {
+ // return ;
+ // },
+ // params: {
+ // query: t.partial({
+ // rangeFrom: t.string,
+ // rangeTo: t.string,
+ // refreshPaused: jsonRt.pipe(t.boolean),
+ // refreshInterval: jsonRt.pipe(t.number),
+ // }),
+ // },
+ // },
};
diff --git a/x-pack/plugins/osquery/cypress/README.md b/x-pack/plugins/osquery/cypress/README.md
new file mode 100644
index 0000000000000..0df311ebc0a05
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/README.md
@@ -0,0 +1,138 @@
+# Cypress Tests
+
+The `osquery/cypress` directory contains functional UI tests that execute using [Cypress](https://www.cypress.io/).
+
+## Running the tests
+
+There are currently three ways to run the tests, comprised of two execution modes and two target environments, which will be detailed below.
+
+### Execution modes
+
+#### Interactive mode
+
+When you run Cypress in interactive mode, an interactive runner is displayed that allows you to see commands as they execute while also viewing the application under test. For more information, please see [cypress documentation](https://docs.cypress.io/guides/core-concepts/test-runner.html#Overview).
+
+#### Headless mode
+
+A headless browser is a browser simulation program that does not have a user interface. These programs operate like any other browser, but do not display any UI. This is why meanwhile you are executing the tests on this mode you are not going to see the application under test. Just the output of the test is displayed on the terminal once the execution is finished.
+
+### Target environments
+
+#### FTR (CI)
+
+This is the configuration used by CI. It uses the FTR to spawn both a Kibana instance (http://localhost:5620) and an Elasticsearch instance (http://localhost:9220) with a preloaded minimum set of data (see preceding "Test data" section), and then executes cypress against this stack. You can find this configuration in `x-pack/test/security_solution_cypress`
+
+### Test Execution: Examples
+
+#### FTR + Headless (Chrome)
+
+Since this is how tests are run on CI, this will likely be the configuration you want to reproduce failures locally, etc.
+
+```shell
+# bootstrap kibana from the project root
+yarn kbn bootstrap
+
+# build the plugins/assets that cypress will execute against
+node scripts/build_kibana_platform_plugins
+
+# launch the cypress test runner
+cd x-pack/plugins/security_solution
+yarn cypress:run-as-ci
+```
+#### FTR + Interactive
+
+This is the preferred mode for developing new tests.
+
+```shell
+# bootstrap kibana from the project root
+yarn kbn bootstrap
+
+# build the plugins/assets that cypress will execute against
+node scripts/build_kibana_platform_plugins
+
+# launch the cypress test runner
+cd x-pack/plugins/security_solution
+yarn cypress:open-as-ci
+```
+
+Note that you can select the browser you want to use on the top right side of the interactive runner.
+
+## Folder Structure
+
+### integration/
+
+Cypress convention. Contains the specs that are going to be executed.
+
+### fixtures/
+
+Cypress convention. Fixtures are used as external pieces of static data when we stub responses.
+
+### plugins/
+
+Cypress convention. As a convenience, by default Cypress will automatically include the plugins file cypress/plugins/index.js before every single spec file it runs.
+
+### screens/
+
+Contains the elements we want to interact with in our tests.
+
+Each file inside the screens folder represents a screen in our application.
+
+### tasks/
+
+_Tasks_ are functions that may be reused across tests.
+
+Each file inside the tasks folder represents a screen of our application.
+
+## Test data
+
+The data the tests need:
+
+- Is generated on the fly using our application APIs (preferred way)
+- Is ingested on the ELS instance using the `es_archive` utility
+
+### How to generate a new archive
+
+**Note:** As mentioned above, archives are only meant to contain external data, e.g. beats data. Due to the tendency for archived domain objects (rules, signals) to quickly become out of date, it is strongly suggested that you generate this data within the test, through interaction with either the UI or the API.
+
+We use es_archiver to manage the data that our Cypress tests need.
+
+1. Set up a clean instance of kibana and elasticsearch (if this is not possible, try to clean/minimize the data that you are going to archive).
+2. With the kibana and elasticsearch instance up and running, create the data that you need for your test.
+3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/security_solution`
+
+```sh
+node ../../../scripts/es_archiver save --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@:
+```
+
+Example:
+
+```sh
+node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220
+```
+
+Note that the command will create the folder if it does not exist.
+
+## Development Best Practices
+
+### Clean up the state
+
+Remember to clean up the state of the test after its execution, typically with the `cleanKibana` function. Be mindful of failure scenarios, as well: if your test fails, will it leave the environment in a recoverable state?
+
+### Minimize the use of es_archive
+
+When possible, create all the data that you need for executing the tests using the application APIS or the UI.
+
+### Speed up test execution time
+
+Loading the web page takes a big amount of time, in order to minimize that impact, the following points should be
+taken into consideration until another solution is implemented:
+
+- Group the tests that are similar in different contexts.
+- For every context login only once, clean the state between tests if needed without re-loading the page.
+- All tests in a spec file must be order-independent.
+
+Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time.
+
+## Linting
+
+Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage)
diff --git a/x-pack/plugins/osquery/cypress/cypress.json b/x-pack/plugins/osquery/cypress/cypress.json
new file mode 100644
index 0000000000000..eb24616607ec3
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/cypress.json
@@ -0,0 +1,14 @@
+{
+ "baseUrl": "http://localhost:5620",
+ "defaultCommandTimeout": 60000,
+ "execTimeout": 120000,
+ "pageLoadTimeout": 120000,
+ "nodeVersion": "system",
+ "retries": {
+ "runMode": 2
+ },
+ "trashAssetsBeforeRuns": false,
+ "video": false,
+ "viewportHeight": 900,
+ "viewportWidth": 1440
+}
\ No newline at end of file
diff --git a/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts
new file mode 100644
index 0000000000000..0babfd2f10a8e
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { HEADER } from '../screens/osquery';
+import { OSQUERY_NAVIGATION_LINK } from '../screens/navigation';
+
+import { INTEGRATIONS, OSQUERY, openNavigationFlyout, navigateTo } from '../tasks/navigation';
+import { addIntegration } from '../tasks/integrations';
+
+describe('Osquery Manager', () => {
+ before(() => {
+ navigateTo(INTEGRATIONS);
+ addIntegration('Osquery Manager');
+ });
+
+ it('Displays Osquery on the navigation flyout once installed ', () => {
+ openNavigationFlyout();
+ cy.get(OSQUERY_NAVIGATION_LINK).should('exist');
+ });
+
+ it('Displays Live queries history title when navigating to Osquery', () => {
+ navigateTo(OSQUERY);
+ cy.get(HEADER).should('have.text', 'Live queries history');
+ });
+});
diff --git a/x-pack/plugins/osquery/cypress/plugins/index.js b/x-pack/plugins/osquery/cypress/plugins/index.js
new file mode 100644
index 0000000000000..7dbb69ced7016
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/plugins/index.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+///
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+module.exports = (_on, _config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+};
diff --git a/x-pack/plugins/osquery/cypress/screens/integrations.ts b/x-pack/plugins/osquery/cypress/screens/integrations.ts
new file mode 100644
index 0000000000000..0b29e857f46ee
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/screens/integrations.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const ADD_POLICY_BTN = '[data-test-subj="addIntegrationPolicyButton"]';
+export const CREATE_PACKAGE_POLICY_SAVE_BTN = '[data-test-subj="createPackagePolicySaveButton"]';
+export const INTEGRATIONS_CARD = '.euiCard__titleAnchor';
diff --git a/x-pack/plugins/osquery/cypress/screens/navigation.ts b/x-pack/plugins/osquery/cypress/screens/navigation.ts
new file mode 100644
index 0000000000000..7884cf347d7c0
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/screens/navigation.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const TOGGLE_NAVIGATION_BTN = '[data-test-subj="toggleNavButton"]';
+export const OSQUERY_NAVIGATION_LINK = '[data-test-subj="collapsibleNavAppLink"] [title="Osquery"]';
diff --git a/x-pack/plugins/osquery/cypress/screens/osquery.ts b/x-pack/plugins/osquery/cypress/screens/osquery.ts
new file mode 100644
index 0000000000000..bc387a57e9e3c
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/screens/osquery.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const HEADER = 'h1';
diff --git a/x-pack/plugins/osquery/cypress/support/commands.js b/x-pack/plugins/osquery/cypress/support/commands.js
new file mode 100644
index 0000000000000..66f9435035571
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/support/commands.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
diff --git a/x-pack/plugins/osquery/cypress/support/index.ts b/x-pack/plugins/osquery/cypress/support/index.ts
new file mode 100644
index 0000000000000..72618c943f4d2
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/support/index.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands';
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+Cypress.on('uncaught:exception', () => {
+ return false;
+});
diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts
new file mode 100644
index 0000000000000..f85ef56550af5
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/tasks/integrations.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ ADD_POLICY_BTN,
+ CREATE_PACKAGE_POLICY_SAVE_BTN,
+ INTEGRATIONS_CARD,
+} from '../screens/integrations';
+
+export const addIntegration = (integration: string) => {
+ cy.get(INTEGRATIONS_CARD).contains(integration).click();
+ cy.get(ADD_POLICY_BTN).click();
+ cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).click();
+ cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).should('not.exist');
+ cy.reload();
+};
diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts
new file mode 100644
index 0000000000000..63d6b205b433b
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/tasks/navigation.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { TOGGLE_NAVIGATION_BTN } from '../screens/navigation';
+
+export const INTEGRATIONS = 'app/integrations#/';
+export const OSQUERY = 'app/osquery/live_queries';
+
+export const navigateTo = (page: string) => {
+ cy.visit(page);
+};
+
+export const openNavigationFlyout = () => {
+ cy.get(TOGGLE_NAVIGATION_BTN).click();
+};
diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json
new file mode 100644
index 0000000000000..467ea13fc4869
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "exclude": [],
+ "include": [
+ "./**/*"
+ ],
+ "compilerOptions": {
+ "tsBuildInfoFile": "../../../../build/tsbuildinfo/osquery/cypress",
+ "types": [
+ "cypress",
+ "node"
+ ],
+ "resolveJsonModule": true,
+ },
+ }
diff --git a/x-pack/plugins/osquery/package.json b/x-pack/plugins/osquery/package.json
new file mode 100644
index 0000000000000..5bbb95e556d6b
--- /dev/null
+++ b/x-pack/plugins/osquery/package.json
@@ -0,0 +1,13 @@
+{
+ "author": "Elastic",
+ "name": "osquery",
+ "version": "8.0.0",
+ "private": true,
+ "license": "Elastic-License",
+ "scripts": {
+ "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json",
+ "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/visual_config.ts",
+ "cypress:run": "../../../node_modules/.bin/cypress run --config-file ./cypress/cypress.json",
+ "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/cli_config.ts"
+ }
+}
diff --git a/x-pack/plugins/osquery/server/usage/fetchers.test.ts b/x-pack/plugins/osquery/server/usage/fetchers.test.ts
new file mode 100644
index 0000000000000..13da639e2c72d
--- /dev/null
+++ b/x-pack/plugins/osquery/server/usage/fetchers.test.ts
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { extractBeatUsageMetrics } from './fetchers';
+
+describe('extractBeatUsageMetrics', () => {
+ it('should not blow when no values are supplied for the aggregations', () => {
+ expect(extractBeatUsageMetrics({})).toEqual({
+ memory: {
+ rss: {},
+ },
+ cpu: {},
+ });
+ });
+
+ it('should not blow when some values are missing from the aggregations', () => {
+ expect(
+ extractBeatUsageMetrics({
+ aggregations: {
+ lastDay: {
+ max_rss: {
+ value: 1,
+ },
+ },
+ },
+ })
+ ).toEqual({
+ memory: {
+ rss: {
+ max: 1,
+ },
+ },
+ cpu: {},
+ });
+ });
+
+ it('should pick out all the max/avg/latest for memory/cpu', () => {
+ expect(
+ extractBeatUsageMetrics({
+ aggregations: {
+ lastDay: {
+ max_rss: {
+ value: 1,
+ },
+ avg_rss: {
+ value: 1,
+ },
+ max_cpu: {
+ value: 2,
+ },
+ avg_cpu: {
+ value: 2,
+ },
+ latest: {
+ hits: {
+ total: 1,
+ hits: [
+ {
+ _index: '',
+ _id: '',
+ _source: {
+ monitoring: {
+ metrics: {
+ beat: {
+ cpu: {
+ total: {
+ time: {
+ ms: 2,
+ },
+ },
+ },
+ memstats: {
+ rss: 1,
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ })
+ ).toEqual({
+ memory: {
+ rss: {
+ max: 1,
+ avg: 1,
+ latest: 1,
+ },
+ },
+ cpu: {
+ max: 2,
+ avg: 2,
+ latest: 2,
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/osquery/server/usage/fetchers.ts b/x-pack/plugins/osquery/server/usage/fetchers.ts
index 5f5b282331bec..3d5f3592101fd 100644
--- a/x-pack/plugins/osquery/server/usage/fetchers.ts
+++ b/x-pack/plugins/osquery/server/usage/fetchers.ts
@@ -9,6 +9,7 @@ import {
AggregationsSingleBucketAggregate,
AggregationsTopHitsAggregate,
AggregationsValueAggregate,
+ SearchResponse,
} from '@elastic/elasticsearch/api/types';
import { PackagePolicyServiceInterface } from '../../../fleet/server';
import { getRouteMetric } from '../routes/usage';
@@ -55,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') {
@@ -117,6 +119,7 @@ export async function getLiveQueryUsage(
},
},
index: '.fleet-actions',
+ ignore_unavailable: true,
});
const result: LiveQueryUsage = {
session: await getRouteMetric(soClient, 'live_query'),
@@ -133,6 +136,46 @@ export async function getLiveQueryUsage(
return result;
}
+export function extractBeatUsageMetrics(
+ metricResponse: Pick, 'aggregations'>
+) {
+ const lastDayAggs = metricResponse.aggregations?.lastDay as AggregationsSingleBucketAggregate;
+ const result: BeatMetricsUsage = {
+ memory: {
+ rss: {},
+ },
+ cpu: {},
+ };
+
+ if (lastDayAggs) {
+ if ('max_rss' in lastDayAggs) {
+ result.memory.rss.max = (lastDayAggs.max_rss as AggregationsValueAggregate).value;
+ }
+
+ if ('avg_rss' in lastDayAggs) {
+ result.memory.rss.avg = (lastDayAggs.max_rss as AggregationsValueAggregate).value;
+ }
+
+ if ('max_cpu' in lastDayAggs) {
+ result.cpu.max = (lastDayAggs.max_cpu as AggregationsValueAggregate).value;
+ }
+
+ if ('avg_cpu' in lastDayAggs) {
+ result.cpu.avg = (lastDayAggs.max_cpu as AggregationsValueAggregate).value;
+ }
+
+ if ('latest' in lastDayAggs) {
+ const latest = (lastDayAggs.latest as AggregationsTopHitsAggregate).hits.hits[0]?._source
+ ?.monitoring.metrics.beat;
+ if (latest) {
+ result.cpu.latest = latest.cpu.total.time.ms;
+ result.memory.rss.latest = latest.memstats.rss;
+ }
+ }
+ }
+ return result;
+}
+
export async function getBeatUsage(esClient: ElasticsearchClient) {
const { body: metricResponse } = await esClient.search({
body: {
@@ -185,39 +228,8 @@ export async function getBeatUsage(esClient: ElasticsearchClient) {
},
},
index: METRICS_INDICES,
+ ignore_unavailable: true,
});
- const lastDayAggs = metricResponse.aggregations?.lastDay as AggregationsSingleBucketAggregate;
- const result: BeatMetricsUsage = {
- memory: {
- rss: {},
- },
- cpu: {},
- };
-
- if ('max_rss' in lastDayAggs) {
- result.memory.rss.max = (lastDayAggs.max_rss as AggregationsValueAggregate).value;
- }
- if ('avg_rss' in lastDayAggs) {
- result.memory.rss.avg = (lastDayAggs.max_rss as AggregationsValueAggregate).value;
- }
-
- if ('max_cpu' in lastDayAggs) {
- result.cpu.max = (lastDayAggs.max_cpu as AggregationsValueAggregate).value;
- }
-
- if ('avg_cpu' in lastDayAggs) {
- result.cpu.avg = (lastDayAggs.max_cpu as AggregationsValueAggregate).value;
- }
-
- if ('latest' in lastDayAggs) {
- const latest = (lastDayAggs.latest as AggregationsTopHitsAggregate).hits.hits[0]?._source
- ?.monitoring.metrics.beat;
- if (latest) {
- result.cpu.latest = latest.cpu.total.time.ms;
- result.memory.rss.latest = latest.memstats.rss;
- }
- }
-
- return result;
+ return extractBeatUsageMetrics(metricResponse);
}
diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts
index 2148cf983d889..8205b4f13a320 100644
--- a/x-pack/plugins/reporting/common/types.ts
+++ b/x-pack/plugins/reporting/common/types.ts
@@ -68,6 +68,7 @@ export interface ReportSource {
};
meta: { objectType: string; layout?: string };
browser_type: string;
+ migration_version: string;
max_attempts: number;
timeout: number;
@@ -77,7 +78,7 @@ export interface ReportSource {
started_at?: string;
completed_at?: string;
created_at: string;
- process_expiration?: string;
+ process_expiration?: string | null; // must be set to null to clear the expiration
}
/*
diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap
index a6753211fba3b..01a8be98bc4be 100644
--- a/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap
+++ b/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap
@@ -64,7 +64,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout
className="euiFormRow__fieldWrapper"
>
-
-
-
- }
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- Unable to fetch report info
-
-
-
-
-
-
-
+
-
-
- Could not fetch the job info
-
-
+ Could not fetch the job info
-
+
-
+
-
-
+
+
,
-
-
-
-
-
-
-
-
-
+
@@ -215,6 +122,7 @@ Array [
>
-
-
-
- }
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- Job Info
-
-
-
-
-
-
-
+ className="euiText euiText--medium"
+ />
+
-
+
-
-
+
+
,
-
-
-
-
-
-
-
-
-
+
@@ -420,6 +235,7 @@ Array [
>
= {
JOB_STATUS_PENDING: 'pending',
JOB_STATUS_PROCESSING: 'processing',
JOB_STATUS_COMPLETED: 'completed',
JOB_STATUS_WARNINGS: 'completed_with_warnings',
JOB_STATUS_FAILED: 'failed',
- JOB_STATUS_CANCELLED: 'cancelled',
};
diff --git a/x-pack/plugins/reporting/server/lib/store/mapping.ts b/x-pack/plugins/reporting/server/lib/store/mapping.ts
index ce8f768ef077f..69f432562ec98 100644
--- a/x-pack/plugins/reporting/server/lib/store/mapping.ts
+++ b/x-pack/plugins/reporting/server/lib/store/mapping.ts
@@ -7,15 +7,10 @@
export const mapping = {
meta: {
- // We are indexing these properties with both text and keyword fields because that's what will be auto generated
- // when an index already exists. This schema is only used when a reporting index doesn't exist. This way existing
- // reporting indexes and new reporting indexes will look the same and the data can be queried in the same
- // manner.
+ // We are indexing these properties with both text and keyword fields
+ // because that's what will be auto generated when an index already exists.
properties: {
- /**
- * Type of object that is triggering this report. Should be either search, visualization or dashboard.
- * Used for job listing and telemetry stats only.
- */
+ // ID of the app this report: search, visualization or dashboard, etc
objectType: {
type: 'text',
fields: {
@@ -25,10 +20,6 @@ export const mapping = {
},
},
},
- /**
- * Can be either preserve_layout, print or none (in the case of csv export).
- * Used for phone home stats only.
- */
layout: {
type: 'text',
fields: {
@@ -41,9 +32,10 @@ export const mapping = {
},
},
browser_type: { type: 'keyword' },
+ migration_version: { type: 'keyword' }, // new field (7.14) to distinguish reports that were scheduled with Task Manager
jobtype: { type: 'keyword' },
payload: { type: 'object', enabled: false },
- priority: { type: 'byte' }, // NOTE: this is unused, but older data may have a mapping for this field
+ priority: { type: 'byte' }, // TODO: remove: this is unused
timeout: { type: 'long' },
process_expiration: { type: 'date' },
created_by: { type: 'keyword' }, // `null` if security is disabled
diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts
index 23d766f2190f6..a8d14e12a738b 100644
--- a/x-pack/plugins/reporting/server/lib/store/report.test.ts
+++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts
@@ -20,21 +20,18 @@ describe('Class Report', () => {
timeout: 30000,
});
- expect(report.toEsDocsJSON()).toMatchObject({
- _index: '.reporting-test-index-12345',
- _source: {
- attempts: 0,
- browser_type: 'browser_type_test_string',
- completed_at: undefined,
- created_by: 'created_by_test_string',
- jobtype: 'test-report',
- max_attempts: 50,
- meta: { objectType: 'test' },
- payload: { headers: 'payload_test_field', objectType: 'testOt' },
- started_at: undefined,
- status: 'pending',
- timeout: 30000,
- },
+ expect(report.toReportSource()).toMatchObject({
+ attempts: 0,
+ browser_type: 'browser_type_test_string',
+ completed_at: undefined,
+ created_by: 'created_by_test_string',
+ jobtype: 'test-report',
+ max_attempts: 50,
+ meta: { objectType: 'test' },
+ payload: { headers: 'payload_test_field', objectType: 'testOt' },
+ started_at: undefined,
+ status: 'pending',
+ timeout: 30000,
});
expect(report.toReportTaskJSON()).toMatchObject({
attempts: 0,
@@ -80,22 +77,18 @@ describe('Class Report', () => {
};
report.updateWithEsDoc(metadata);
- expect(report.toEsDocsJSON()).toMatchObject({
- _id: '12342p9o387549o2345',
- _index: '.reporting-test-update',
- _source: {
- attempts: 0,
- browser_type: 'browser_type_test_string',
- completed_at: undefined,
- created_by: 'created_by_test_string',
- jobtype: 'test-report',
- max_attempts: 50,
- meta: { objectType: 'stange' },
- payload: { objectType: 'testOt' },
- started_at: undefined,
- status: 'pending',
- timeout: 30000,
- },
+ expect(report.toReportSource()).toMatchObject({
+ attempts: 0,
+ browser_type: 'browser_type_test_string',
+ completed_at: undefined,
+ created_by: 'created_by_test_string',
+ jobtype: 'test-report',
+ max_attempts: 50,
+ meta: { objectType: 'stange' },
+ payload: { objectType: 'testOt' },
+ started_at: undefined,
+ status: 'pending',
+ timeout: 30000,
});
expect(report.toReportTaskJSON()).toMatchObject({
attempts: 0,
diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts
index 9b98650e1d984..fa5b91527ccc4 100644
--- a/x-pack/plugins/reporting/server/lib/store/report.ts
+++ b/x-pack/plugins/reporting/server/lib/store/report.ts
@@ -21,8 +21,13 @@ export { ReportDocument };
export { ReportApiJSON, ReportSource };
const puid = new Puid();
+export const MIGRATION_VERSION = '7.14.0';
-export class Report implements Partial
{
+/*
+ * The public fields are a flattened version what Elasticsearch returns when you
+ * `GET` a document.
+ */
+export class Report implements Partial {
public _index?: string;
public _id: string;
public _primary_term?: number; // set by ES
@@ -47,6 +52,7 @@ export class Report implements Partial {
public readonly timeout?: ReportSource['timeout'];
public process_expiration?: ReportSource['process_expiration'];
+ public migration_version: string;
/*
* Create an unsaved report
@@ -58,6 +64,8 @@ export class Report implements Partial {
this._primary_term = opts._primary_term;
this._seq_no = opts._seq_no;
+ this.migration_version = MIGRATION_VERSION;
+
this.payload = opts.payload!;
this.kibana_name = opts.kibana_name!;
this.kibana_id = opts.kibana_id!;
@@ -80,7 +88,7 @@ export class Report implements Partial {
/*
* Update the report with "live" storage metadata
*/
- updateWithEsDoc(doc: Partial) {
+ updateWithEsDoc(doc: Partial): void {
if (doc._index == null || doc._id == null) {
throw new Error(`Report object from ES has missing fields!`);
}
@@ -89,30 +97,31 @@ export class Report implements Partial {
this._index = doc._index;
this._primary_term = doc._primary_term;
this._seq_no = doc._seq_no;
+ this.migration_version = MIGRATION_VERSION;
}
/*
* Data structure for writing to Elasticsearch index
*/
- toEsDocsJSON() {
+ toReportSource(): ReportSource {
return {
- _id: this._id,
- _index: this._index,
- _source: {
- jobtype: this.jobtype,
- created_at: this.created_at,
- created_by: this.created_by,
- payload: this.payload,
- meta: this.meta,
- timeout: this.timeout,
- max_attempts: this.max_attempts,
- browser_type: this.browser_type,
- status: this.status,
- attempts: this.attempts,
- started_at: this.started_at,
- completed_at: this.completed_at,
- process_expiration: this.process_expiration,
- },
+ migration_version: MIGRATION_VERSION,
+ kibana_name: this.kibana_name,
+ kibana_id: this.kibana_id,
+ jobtype: this.jobtype,
+ created_at: this.created_at,
+ created_by: this.created_by,
+ payload: this.payload,
+ meta: this.meta,
+ timeout: this.timeout!,
+ max_attempts: this.max_attempts,
+ browser_type: this.browser_type!,
+ status: this.status,
+ attempts: this.attempts,
+ started_at: this.started_at,
+ completed_at: this.completed_at,
+ process_expiration: this.process_expiration,
+ output: this.output || null,
};
}
diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts
index 7f96433fcc6ce..8bb5c7fb8bbf9 100644
--- a/x-pack/plugins/reporting/server/lib/store/store.test.ts
+++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts
@@ -184,6 +184,7 @@ describe('ReportingStore', () => {
_source: {
kibana_name: 'test',
kibana_id: 'test123',
+ migration_version: 'X.0.0',
created_at: 'some time',
created_by: 'some security person',
jobtype: 'csv',
@@ -222,6 +223,7 @@ describe('ReportingStore', () => {
"meta": Object {
"testMeta": "meta",
},
+ "migration_version": "7.14.0",
"output": null,
"payload": Object {
"testPayload": "payload",
@@ -239,6 +241,8 @@ describe('ReportingStore', () => {
const report = new Report({
_id: 'id-of-processing',
_index: '.reporting-test-index-12345',
+ _seq_no: 42,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -254,24 +258,12 @@ describe('ReportingStore', () => {
await store.setReportClaimed(report, { testDoc: 'test' } as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "status": "processing",
- "testDoc": "test",
- },
- },
- "id": "id-of-processing",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
- `);
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`processing`);
+ expect(updateCall.if_seq_no).toBe(42);
+ expect(updateCall.if_primary_term).toBe(10002);
});
it('setReportFailed sets the status of a record to failed', async () => {
@@ -279,6 +271,8 @@ describe('ReportingStore', () => {
const report = new Report({
_id: 'id-of-failure',
_index: '.reporting-test-index-12345',
+ _seq_no: 43,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -294,24 +288,12 @@ describe('ReportingStore', () => {
await store.setReportFailed(report, { errors: 'yes' } as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "errors": "yes",
- "status": "failed",
- },
- },
- "id": "id-of-failure",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
- `);
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`failed`);
+ expect(updateCall.if_seq_no).toBe(43);
+ expect(updateCall.if_primary_term).toBe(10002);
});
it('setReportCompleted sets the status of a record to completed', async () => {
@@ -319,6 +301,8 @@ describe('ReportingStore', () => {
const report = new Report({
_id: 'vastly-great-report-id',
_index: '.reporting-test-index-12345',
+ _seq_no: 44,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -334,31 +318,21 @@ describe('ReportingStore', () => {
await store.setReportCompleted(report, { certainly_completed: 'yes' } as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "certainly_completed": "yes",
- "status": "completed",
- },
- },
- "id": "vastly-great-report-id",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
- `);
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`completed`);
+ expect(updateCall.if_seq_no).toBe(44);
+ expect(updateCall.if_primary_term).toBe(10002);
});
- it('setReportCompleted sets the status of a record to completed_with_warnings', async () => {
+ it('sets the status of a record to completed_with_warnings', async () => {
const store = new ReportingStore(mockCore, mockLogger);
const report = new Report({
_id: 'vastly-great-report-id',
_index: '.reporting-test-index-12345',
+ _seq_no: 45,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -379,28 +353,52 @@ describe('ReportingStore', () => {
},
} as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "certainly_completed": "pretty_much",
- "output": Object {
- "warnings": Array [
- "those pants don't go with that shirt",
- ],
- },
- "status": "completed_with_warnings",
- },
- },
- "id": "vastly-great-report-id",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`completed_with_warnings`);
+ expect(updateCall.if_seq_no).toBe(45);
+ expect(updateCall.if_primary_term).toBe(10002);
+ expect(response.output).toMatchInlineSnapshot(`
+ Object {
+ "warnings": Array [
+ "those pants don't go with that shirt",
+ ],
+ }
`);
});
+
+ it('prepareReportForRetry resets the expiration and status on the report document', async () => {
+ const store = new ReportingStore(mockCore, mockLogger);
+ const report = new Report({
+ _id: 'pretty-good-report-id',
+ _index: '.reporting-test-index-94058763',
+ _seq_no: 46,
+ _primary_term: 10002,
+ jobtype: 'test-report-2',
+ created_by: 'created_by_test_string',
+ browser_type: 'browser_type_test_string',
+ status: 'processing',
+ process_expiration: '2002',
+ max_attempts: 3,
+ payload: {
+ title: 'test report',
+ headers: 'rp_test_headers',
+ objectType: 'testOt',
+ browserTimezone: 'utc',
+ },
+ timeout: 30000,
+ });
+
+ await store.prepareReportForRetry(report);
+
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`pending`);
+ expect(updateCall.if_seq_no).toBe(46);
+ expect(updateCall.if_primary_term).toBe(10002);
+ });
});
diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts
index fc7bd9c23d769..8f1e6c315a2d1 100644
--- a/x-pack/plugins/reporting/server/lib/store/store.ts
+++ b/x-pack/plugins/reporting/server/lib/store/store.ts
@@ -5,15 +5,38 @@
* 2.0.
*/
+import { IndexResponse, UpdateResponse } from '@elastic/elasticsearch/api/types';
import { ElasticsearchClient } from 'src/core/server';
import { LevelLogger, statuses } from '../';
import { ReportingCore } from '../../';
-import { numberToDuration } from '../../../common/schema_utils';
import { JobStatus } from '../../../common/types';
import { ReportTaskParams } from '../tasks';
import { indexTimestamp } from './index_timestamp';
import { mapping } from './mapping';
-import { Report, ReportDocument, ReportSource } from './report';
+import { MIGRATION_VERSION, Report, ReportDocument, ReportSource } from './report';
+
+/*
+ * When an instance of Kibana claims a report job, this information tells us about that instance
+ */
+export type ReportProcessingFields = Required<{
+ kibana_id: Report['kibana_id'];
+ kibana_name: Report['kibana_name'];
+ browser_type: Report['browser_type'];
+ attempts: Report['attempts'];
+ started_at: Report['started_at'];
+ timeout: Report['timeout'];
+ process_expiration: Report['process_expiration'];
+}>;
+
+export type ReportFailedFields = Required<{
+ completed_at: Report['completed_at'];
+ output: Report['output'];
+}>;
+
+export type ReportCompletedFields = Required<{
+ completed_at: Report['completed_at'];
+ output: Report['output'];
+}>;
/*
* When searching for long-pending reports, we get a subset of fields
@@ -24,15 +47,38 @@ export interface ReportRecordTimeout {
_source: {
status: JobStatus;
process_expiration?: string;
- created_at?: string;
};
}
const checkReportIsEditable = (report: Report) => {
- if (!report._id || !report._index) {
- throw new Error(`Report object is not synced with ES!`);
+ const { _id, _index, _seq_no, _primary_term } = report;
+ if (_id == null || _index == null) {
+ throw new Error(`Report is not editable: Job [${_id}] is not synced with ES!`);
+ }
+
+ if (_seq_no == null || _primary_term == null) {
+ throw new Error(
+ `Report is not editable: Job [${_id}] is missing _seq_no and _primary_term fields!`
+ );
}
};
+/*
+ * When searching for long-pending reports, we get a subset of fields
+ */
+const sourceDoc = (doc: Partial): Partial => {
+ return {
+ ...doc,
+ migration_version: MIGRATION_VERSION,
+ };
+};
+
+const jobDebugMessage = (report: Report) =>
+ `${report._id} ` +
+ `[_index: ${report._index}] ` +
+ `[_seq_no: ${report._seq_no}] ` +
+ `[_primary_term: ${report._primary_term}]` +
+ `[attempts: ${report.attempts}] ` +
+ `[process_expiration: ${report.process_expiration}]`;
/*
* A class to give an interface to historical reports in the reporting.index
@@ -43,7 +89,6 @@ const checkReportIsEditable = (report: Report) => {
export class ReportingStore {
private readonly indexPrefix: string; // config setting of index prefix in system index name
private readonly indexInterval: string; // config setting of index prefix: how often to poll for pending work
- private readonly queueTimeoutMins: number; // config setting of queue timeout, rounded up to nearest minute
private client?: ElasticsearchClient;
constructor(private reportingCore: ReportingCore, private logger: LevelLogger) {
@@ -52,7 +97,6 @@ export class ReportingStore {
this.indexPrefix = config.get('index');
this.indexInterval = config.get('queue', 'indexInterval');
this.logger = logger.clone(['store']);
- this.queueTimeoutMins = Math.ceil(numberToDuration(config.get('queue', 'timeout')).asMinutes());
}
private async getClient() {
@@ -103,18 +147,20 @@ export class ReportingStore {
/*
* Called from addReport, which handles any errors
*/
- private async indexReport(report: Report) {
+ private async indexReport(report: Report): Promise {
const doc = {
index: report._index!,
id: report._id,
+ refresh: true,
body: {
- ...report.toEsDocsJSON()._source,
- process_expiration: new Date(0), // use epoch so the job query works
- attempts: 0,
- status: statuses.JOB_STATUS_PENDING,
+ ...report.toReportSource(),
+ ...sourceDoc({
+ process_expiration: new Date(0).toISOString(),
+ attempts: 0,
+ status: statuses.JOB_STATUS_PENDING,
+ }),
},
};
-
const client = await this.getClient();
const { body } = await client.index(doc);
@@ -140,8 +186,7 @@ export class ReportingStore {
await this.createIndex(index);
try {
- const doc = await this.indexReport(report);
- report.updateWithEsDoc(doc);
+ report.updateWithEsDoc(await this.indexReport(report));
await this.refreshIndex(index);
@@ -156,7 +201,9 @@ export class ReportingStore {
/*
* Search for a report from task data and return back the report
*/
- public async findReportFromTask(taskJson: ReportTaskParams): Promise {
+ public async findReportFromTask(
+ taskJson: Pick
+ ): Promise {
if (!taskJson.index) {
throw new Error('Task JSON is missing index field!');
}
@@ -186,41 +233,23 @@ export class ReportingStore {
timeout: document._source?.timeout,
});
} catch (err) {
- this.logger.error('Error in finding a report! ' + JSON.stringify({ report: taskJson }));
- this.logger.error(err);
- throw err;
- }
- }
-
- public async setReportPending(report: Report) {
- const doc = { status: statuses.JOB_STATUS_PENDING };
-
- try {
- checkReportIsEditable(report);
-
- const client = await this.getClient();
- const { body } = await client.update({
- id: report._id,
- index: report._index!,
- if_seq_no: report._seq_no,
- if_primary_term: report._primary_term,
- refresh: true,
- body: { doc },
- });
-
- return (body as unknown) as ReportDocument;
- } catch (err) {
- this.logger.error('Error in setting report pending status!');
+ this.logger.error(
+ `Error in finding the report from the scheduled task info! ` +
+ `[id: ${taskJson.id}] [index: ${taskJson.index}]`
+ );
this.logger.error(err);
throw err;
}
}
- public async setReportClaimed(report: Report, stats: Partial): Promise {
- const doc = {
- ...stats,
+ public async setReportClaimed(
+ report: Report,
+ processingInfo: ReportProcessingFields
+ ): Promise> {
+ const doc = sourceDoc({
+ ...processingInfo,
status: statuses.JOB_STATUS_PROCESSING,
- };
+ });
try {
checkReportIsEditable(report);
@@ -235,19 +264,24 @@ export class ReportingStore {
body: { doc },
});
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in setting report processing status!');
+ this.logger.error(
+ `Error in updating status to processing! Report: ` + jobDebugMessage(report)
+ );
this.logger.error(err);
throw err;
}
}
- public async setReportFailed(report: Report, stats: Partial): Promise {
- const doc = {
- ...stats,
+ public async setReportFailed(
+ report: Report,
+ failedInfo: ReportFailedFields
+ ): Promise> {
+ const doc = sourceDoc({
+ ...failedInfo,
status: statuses.JOB_STATUS_FAILED,
- };
+ });
try {
checkReportIsEditable(report);
@@ -261,26 +295,29 @@ export class ReportingStore {
refresh: true,
body: { doc },
});
-
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in setting report failed status!');
+ this.logger.error(`Error in updating status to failed! Report: ` + jobDebugMessage(report));
this.logger.error(err);
throw err;
}
}
- public async setReportCompleted(report: Report, stats: Partial): Promise {
+ public async setReportCompleted(
+ report: Report,
+ completedInfo: ReportCompletedFields
+ ): Promise> {
+ const { output } = completedInfo;
+ const status =
+ output && output.warnings && output.warnings.length > 0
+ ? statuses.JOB_STATUS_WARNINGS
+ : statuses.JOB_STATUS_COMPLETED;
+ const doc = sourceDoc({
+ ...completedInfo,
+ status,
+ });
+
try {
- const { output } = stats;
- const status =
- output && output.warnings && output.warnings.length > 0
- ? statuses.JOB_STATUS_WARNINGS
- : statuses.JOB_STATUS_COMPLETED;
- const doc = {
- ...stats,
- status,
- };
checkReportIsEditable(report);
const client = await this.getClient();
@@ -292,16 +329,20 @@ export class ReportingStore {
refresh: true,
body: { doc },
});
-
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in setting report complete status!');
+ this.logger.error(`Error in updating status to complete! Report: ` + jobDebugMessage(report));
this.logger.error(err);
throw err;
}
}
- public async clearExpiration(report: Report): Promise {
+ public async prepareReportForRetry(report: Report): Promise> {
+ const doc = sourceDoc({
+ status: statuses.JOB_STATUS_PENDING,
+ process_expiration: null,
+ });
+
try {
checkReportIsEditable(report);
@@ -312,50 +353,54 @@ export class ReportingStore {
if_seq_no: report._seq_no,
if_primary_term: report._primary_term,
refresh: true,
- body: { doc: { process_expiration: null } },
+ body: { doc },
});
-
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in clearing expiration!');
+ this.logger.error(
+ `Error in clearing expiration and status for retry! Report: ` + jobDebugMessage(report)
+ );
this.logger.error(err);
throw err;
}
}
/*
- * A zombie report document is one that isn't completed or failed, isn't
- * being executed, and isn't scheduled to run. They arise:
- * - when the cluster has processing documents in ESQueue before upgrading to v7.13 when ESQueue was removed
- * - if Kibana crashes while a report task is executing and it couldn't be rescheduled on its own
- *
- * Pending reports are not included in this search: they may be scheduled in TM just not run yet.
- * TODO Should we get a list of the reports that are pending and scheduled in TM so we can exclude them from this query?
+ * A report needs to be rescheduled when:
+ * 1. An older version of Kibana created jobs with ESQueue, and they have
+ * not yet started running.
+ * 2. The report process_expiration field is overdue, which happens if the
+ * report runs too long or Kibana restarts during execution
*/
- public async findZombieReportDocuments(): Promise {
+ public async findStaleReportJob(): Promise {
const client = await this.getClient();
+
+ const expiredFilter = {
+ bool: {
+ must: [
+ { range: { process_expiration: { lt: `now` } } },
+ { terms: { status: [statuses.JOB_STATUS_PROCESSING] } },
+ ],
+ },
+ };
+ const oldVersionFilter = {
+ bool: {
+ must: [{ terms: { status: [statuses.JOB_STATUS_PENDING] } }],
+ must_not: [{ exists: { field: 'migration_version' } }],
+ },
+ };
+
const { body } = await client.search({
+ size: 1,
index: this.indexPrefix + '-*',
- filter_path: 'hits.hits',
+ seq_no_primary_term: true,
+ _source_excludes: ['output'],
body: {
- sort: { created_at: { order: 'desc' } },
- query: {
- bool: {
- filter: [
- {
- bool: {
- must: [
- { range: { process_expiration: { lt: `now-${this.queueTimeoutMins}m` } } },
- { terms: { status: [statuses.JOB_STATUS_PROCESSING] } },
- ],
- },
- },
- ],
- },
- },
+ sort: { created_at: { order: 'asc' as const } }, // find the oldest first
+ query: { bool: { filter: { bool: { should: [expiredFilter, oldVersionFilter] } } } },
},
});
- return body.hits?.hits as ReportRecordTimeout[];
+ return body.hits?.hits[0] as ReportRecordTimeout;
}
}
diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
index 2960ce457b7ae..f9e2cd82b0805 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { UpdateResponse } from '@elastic/elasticsearch/api/types';
import moment from 'moment';
import * as Rx from 'rxjs';
import { timeout } from 'rxjs/operators';
@@ -19,9 +20,9 @@ import { CancellationToken } from '../../../common';
import { durationToNumber, numberToDuration } from '../../../common/schema_utils';
import { ReportingConfigType } from '../../config';
import { BasePayload, RunTaskFn } from '../../types';
-import { Report, ReportingStore } from '../store';
+import { Report, ReportDocument, ReportingStore } from '../store';
+import { ReportFailedFields, ReportProcessingFields } from '../store/store';
import {
- ReportingExecuteTaskInstance,
ReportingTask,
ReportingTaskStatus,
REPORTING_EXECUTE_TYPE,
@@ -30,6 +31,13 @@ import {
} from './';
import { errorLogger } from './error_logger';
+interface ReportingExecuteTaskInstance {
+ state: object;
+ taskType: string;
+ params: ReportTaskParams;
+ runAt?: Date;
+}
+
function isOutput(output: TaskRunResult | Error): output is TaskRunResult {
return typeof output === 'object' && (output as TaskRunResult).content != null;
}
@@ -101,15 +109,21 @@ export class ExecuteReportTask implements ReportingTask {
}
public async _claimJob(task: ReportTaskParams): Promise {
- const store = await this.getStore();
+ if (this.kibanaId == null) {
+ throw new Error(`Kibana instance ID is undefined!`);
+ }
+ if (this.kibanaName == null) {
+ throw new Error(`Kibana instance name is undefined!`);
+ }
+ const store = await this.getStore();
let report: Report;
if (task.id && task.index) {
// if this is an ad-hoc report, there is a corresponding "pending" record in ReportingStore in need of updating
- report = await store.findReportFromTask(task); // update seq_no
+ report = await store.findReportFromTask(task); // receives seq_no and primary_term
} else {
// if this is a scheduled report (not implemented), the report object needs to be instantiated
- throw new Error('scheduled reports are not supported!');
+ throw new Error('Could not find matching report document!');
}
// Check if this is a completed job. This may happen if the `reports:monitor`
@@ -126,7 +140,7 @@ export class ExecuteReportTask implements ReportingTask {
const maxAttempts = task.max_attempts;
if (report.attempts >= maxAttempts) {
const err = new Error(`Max attempts reached (${maxAttempts}). Queue timeout reached.`);
- await this._failJob(task, err);
+ await this._failJob(report, err);
throw err;
}
@@ -134,7 +148,7 @@ export class ExecuteReportTask implements ReportingTask {
const startTime = m.toISOString();
const expirationTime = m.add(queueTimeout).toISOString();
- const stats = {
+ const doc: ReportProcessingFields = {
kibana_id: this.kibanaId,
kibana_name: this.kibanaName,
browser_type: this.config.capture.browser.type,
@@ -144,19 +158,28 @@ export class ExecuteReportTask implements ReportingTask {
process_expiration: expirationTime,
};
- this.logger.debug(`Claiming ${report.jobtype} job ${report._id}`);
-
const claimedReport = new Report({
...report,
- ...stats,
+ ...doc,
});
- await store.setReportClaimed(claimedReport, stats);
+ this.logger.debug(
+ `Claiming ${claimedReport.jobtype} ${report._id} ` +
+ `[_index: ${report._index}] ` +
+ `[_seq_no: ${report._seq_no}] ` +
+ `[_primary_term: ${report._primary_term}] ` +
+ `[attempts: ${report.attempts}] ` +
+ `[process_expiration: ${expirationTime}]`
+ );
+
+ const resp = await store.setReportClaimed(claimedReport, doc);
+ claimedReport._seq_no = resp._seq_no;
+ claimedReport._primary_term = resp._primary_term;
return claimedReport;
}
- private async _failJob(task: ReportTaskParams, error?: Error) {
- const message = `Failing ${task.jobtype} job ${task.id}`;
+ private async _failJob(report: Report, error?: Error): Promise> {
+ const message = `Failing ${report.jobtype} job ${report._id}`;
// log the error
let docOutput;
@@ -169,9 +192,8 @@ export class ExecuteReportTask implements ReportingTask {
// update the report in the store
const store = await this.getStore();
- const report = await store.findReportFromTask(task);
const completedTime = moment().toISOString();
- const doc = {
+ const doc: ReportFailedFields = {
completed_at: completedTime,
output: docOutput,
};
@@ -179,7 +201,7 @@ export class ExecuteReportTask implements ReportingTask {
return await store.setReportFailed(report, doc);
}
- private _formatOutput(output: TaskRunResult | Error) {
+ private _formatOutput(output: TaskRunResult | Error): TaskRunResult {
const docOutput = {} as TaskRunResult;
const unknownMime = null;
@@ -201,7 +223,10 @@ export class ExecuteReportTask implements ReportingTask {
return docOutput;
}
- public async _performJob(task: ReportTaskParams, cancellationToken: CancellationToken) {
+ public async _performJob(
+ task: ReportTaskParams,
+ cancellationToken: CancellationToken
+ ): Promise {
if (!this.taskExecutors) {
throw new Error(`Task run function factories have not been called yet!`);
}
@@ -220,10 +245,10 @@ export class ExecuteReportTask implements ReportingTask {
.toPromise();
}
- public async _completeJob(task: ReportTaskParams, output: TaskRunResult) {
- let docId = `/${task.index}/_doc/${task.id}`;
+ public async _completeJob(report: Report, output: TaskRunResult): Promise {
+ let docId = `/${report._index}/_doc/${report._id}`;
- this.logger.info(`Saving ${task.jobtype} job ${docId}.`);
+ this.logger.debug(`Saving ${report.jobtype} to ${docId}.`);
const completedTime = moment().toISOString();
const docOutput = this._formatOutput(output);
@@ -233,16 +258,13 @@ export class ExecuteReportTask implements ReportingTask {
completed_at: completedTime,
output: docOutput,
};
- const report = await store.findReportFromTask(task); // update seq_no and primary_term
docId = `/${report._index}/_doc/${report._id}`;
- try {
- await store.setReportCompleted(report, doc);
- this.logger.debug(`Saved ${report.jobtype} job ${docId}`);
- } catch (err) {
- if (err.statusCode === 409) return false;
- errorLogger(this.logger, `Failure saving completed job ${docId}!`);
- }
+ const resp = await store.setReportCompleted(report, doc);
+ this.logger.info(`Saved ${report.jobtype} job ${docId}`);
+ report._seq_no = resp._seq_no;
+ report._primary_term = resp._primary_term;
+ return report;
}
/*
@@ -264,7 +286,6 @@ export class ExecuteReportTask implements ReportingTask {
*/
run: async () => {
let report: Report | undefined;
- let attempts = 0;
// find the job in the store and set status to processing
const task = context.taskInstance.params as ReportTaskParams;
@@ -278,64 +299,73 @@ export class ExecuteReportTask implements ReportingTask {
// Update job status to claimed
report = await this._claimJob(task);
-
- const { jobtype: jobType, attempts: attempt, max_attempts: maxAttempts } = task;
- this.logger.info(
- `Starting ${jobType} report ${jobId}: attempt ${attempt + 1} of ${maxAttempts}.`
- );
- this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
} catch (failedToClaim) {
// error claiming report - log the error
// could be version conflict, or no longer connected to ES
- errorLogger(this.logger, `Error in claiming report!`, failedToClaim);
+ errorLogger(this.logger, `Error in claiming ${jobId}`, failedToClaim);
}
if (!report) {
- errorLogger(this.logger, `Report could not be claimed. Exiting...`);
+ errorLogger(this.logger, `Job ${jobId} could not be claimed. Exiting...`);
return;
}
- attempts = report.attempts;
+ const { jobtype: jobType, attempts, max_attempts: maxAttempts } = report;
+ this.logger.debug(
+ `Starting ${jobType} report ${jobId}: attempt ${attempts} of ${maxAttempts}.`
+ );
+ this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
try {
const output = await this._performJob(task, cancellationToken);
if (output) {
- await this._completeJob(task, output);
+ report = await this._completeJob(report, output);
}
-
// untrack the report for concurrency awareness
this.logger.debug(`Stopping ${jobId}.`);
- this.reporting.untrackReport(jobId);
- this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
} catch (failedToExecuteErr) {
cancellationToken.cancel();
- const maxAttempts = this.config.capture.maxAttempts;
if (attempts < maxAttempts) {
- // attempts remain - reschedule
+ // attempts remain, reschedule
try {
+ if (report == null) {
+ throw new Error(`Report ${jobId} is null!`);
+ }
// reschedule to retry
const remainingAttempts = maxAttempts - report.attempts;
errorLogger(
this.logger,
- `Scheduling retry. Retries remaining: ${remainingAttempts}.`,
+ `Scheduling retry for job ${jobId}. Retries remaining: ${remainingAttempts}.`,
failedToExecuteErr
);
await this.rescheduleTask(reportFromTask(task).toReportTaskJSON(), this.logger);
} catch (rescheduleErr) {
// can not be rescheduled - log the error
- errorLogger(this.logger, `Could not reschedule the errored job!`, rescheduleErr);
+ errorLogger(
+ this.logger,
+ `Could not reschedule the errored job ${jobId}!`,
+ rescheduleErr
+ );
}
} else {
// 0 attempts remain - fail the job
try {
- const maxAttemptsMsg = `Max attempts reached (${attempts}). Failed with: ${failedToExecuteErr}`;
- await this._failJob(task, new Error(maxAttemptsMsg));
+ const maxAttemptsMsg = `Max attempts (${attempts}) reached for job ${jobId}. Failed with: ${failedToExecuteErr}`;
+ if (report == null) {
+ throw new Error(`Report ${jobId} is null!`);
+ }
+ const resp = await this._failJob(report, new Error(maxAttemptsMsg));
+ report._seq_no = resp._seq_no;
+ report._primary_term = resp._primary_term;
} catch (failedToFailError) {
- errorLogger(this.logger, `Could not fail the job!`, failedToFailError);
+ errorLogger(this.logger, `Could not fail ${jobId}!`, failedToFailError);
}
}
+ } finally {
+ this.reporting.untrackReport(jobId);
+ this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
}
},
@@ -374,11 +404,12 @@ export class ExecuteReportTask implements ReportingTask {
state: {},
params: report,
};
+
return await this.getTaskManagerStart().schedule(taskInstance);
}
private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) {
- logger.info(`Rescheduling ${task.id} to retry after error.`);
+ logger.info(`Rescheduling task:${task.id} to retry after error.`);
const oldTaskInstance: ReportingExecuteTaskInstance = {
taskType: REPORTING_EXECUTE_TYPE,
@@ -386,7 +417,7 @@ export class ExecuteReportTask implements ReportingTask {
params: task,
};
const newTask = await this.getTaskManagerStart().schedule(oldTaskInstance);
- logger.debug(`Rescheduled ${task.id}`);
+ logger.debug(`Rescheduled task:${task.id}. New task: task:${newTask.id}`);
return newTask;
}
diff --git a/x-pack/plugins/reporting/server/lib/tasks/index.ts b/x-pack/plugins/reporting/server/lib/tasks/index.ts
index ec9e85e957d03..c02b06d97adc7 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/index.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/index.ts
@@ -32,13 +32,6 @@ export interface ReportTaskParams {
meta: ReportSource['meta'];
}
-export interface ReportingExecuteTaskInstance /* extends TaskInstanceWithDeprecatedFields */ {
- state: object;
- taskType: string;
- params: ReportTaskParams;
- runAt?: Date;
-}
-
export enum ReportingTaskStatus {
UNINITIALIZED = 'uninitialized',
INITIALIZED = 'initialized',
@@ -52,6 +45,5 @@ export interface ReportingTask {
maxAttempts: number;
timeout: string;
};
-
getStatus: () => ReportingTaskStatus;
}
diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
index 36380f767e6d9..9e1bc49739c93 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
@@ -11,21 +11,29 @@ import { ReportingCore } from '../../';
import { TaskManagerStartContract, TaskRunCreatorFunction } from '../../../../task_manager/server';
import { numberToDuration } from '../../../common/schema_utils';
import { ReportingConfigType } from '../../config';
+import { statuses } from '../statuses';
import { Report } from '../store';
-import {
- ReportingExecuteTaskInstance,
- ReportingTask,
- ReportingTaskStatus,
- REPORTING_EXECUTE_TYPE,
- REPORTING_MONITOR_TYPE,
- ReportTaskParams,
-} from './';
+import { ReportingTask, ReportingTaskStatus, REPORTING_MONITOR_TYPE, ReportTaskParams } from './';
/*
- * Task for finding the ReportingRecords left in the ReportingStore and stuck
- * in pending or processing. It could happen if the server crashed while running
- * a report and was cancelled. Normally a failure would mean scheduling a
- * retry or failing the report, but the retry is not guaranteed to be scheduled.
+ * Task for finding the ReportingRecords left in the ReportingStore (.reporting index) and stuck in
+ * a pending or processing status.
+ *
+ * Stuck in pending:
+ * - This can happen if the report was scheduled in an earlier version of Kibana that used ESQueue.
+ * - Task Manager doesn't know about these types of reports because there was never a task
+ * scheduled for them.
+ * Stuck in processing:
+ * - This can could happen if the server crashed while a report was executing.
+ * - Task Manager doesn't know about these reports, because the task is completed in Task
+ * Manager when Reporting starts executing the report. We are not using Task Manager's retry
+ * mechanisms, which defer the retry for a few minutes.
+ *
+ * These events require us to reschedule the report with Task Manager, so that the jobs can be
+ * distributed and executed.
+ *
+ * The runner function reschedules a single report job per task run, to avoid flooding Task Manager
+ * in case many report jobs need to be recovered.
*/
export class MonitorReportsTask implements ReportingTask {
public TYPE = REPORTING_MONITOR_TYPE;
@@ -77,36 +85,41 @@ export class MonitorReportsTask implements ReportingTask {
const reportingStore = await this.getStore();
try {
- const results = await reportingStore.findZombieReportDocuments();
- if (results && results.length) {
- this.logger.info(
- `Found ${results.length} reports to reschedule: ${results
- .map((pending) => pending._id)
- .join(',')}`
- );
- } else {
- this.logger.debug(`Found 0 pending reports.`);
+ const recoveredJob = await reportingStore.findStaleReportJob();
+ if (!recoveredJob) {
+ // no reports need to be rescheduled
return;
}
- for (const pending of results) {
- const {
- _id: jobId,
- _source: { process_expiration: processExpiration, status },
- } = pending;
- const expirationTime = moment(processExpiration); // If it is the start of the Epoch, something went wrong
- const timeWaitValue = moment().valueOf() - expirationTime.valueOf();
- const timeWaitTime = moment.duration(timeWaitValue);
+ const {
+ _id: jobId,
+ _source: { process_expiration: processExpiration, status },
+ } = recoveredJob;
+
+ if (![statuses.JOB_STATUS_PENDING, statuses.JOB_STATUS_PROCESSING].includes(status)) {
+ throw new Error(`Invalid job status in the monitoring search result: ${status}`); // only pending or processing jobs possibility need rescheduling
+ }
+
+ if (status === statuses.JOB_STATUS_PENDING) {
this.logger.info(
- `Task ${jobId} has ${status} status for ${timeWaitTime.humanize()}. The queue timeout is ${this.timeout.humanize()}.`
+ `${jobId} was scheduled in a previous version and left in [${status}] status. Rescheduling...`
);
+ }
- // clear process expiration and reschedule
- const oldReport = new Report({ ...pending, ...pending._source });
- const reschedulingTask = oldReport.toReportTaskJSON();
- await reportingStore.clearExpiration(oldReport);
- await this.rescheduleTask(reschedulingTask, this.logger);
+ if (status === statuses.JOB_STATUS_PROCESSING) {
+ const expirationTime = moment(processExpiration);
+ const overdueValue = moment().valueOf() - expirationTime.valueOf();
+ this.logger.info(
+ `${jobId} status is [${status}] and the expiration time was [${overdueValue}ms] ago. Rescheduling...`
+ );
}
+
+ // clear process expiration and set status to pending
+ const report = new Report({ ...recoveredJob, ...recoveredJob._source });
+ await reportingStore.prepareReportForRetry(report); // if there is a version conflict response, this just throws and logs an error
+
+ // clear process expiration and reschedule
+ await this.rescheduleTask(report.toReportTaskJSON(), this.logger); // a recovered report job must be scheduled by only a sinle Kibana instance
} catch (err) {
this.logger.error(err);
}
@@ -126,33 +139,19 @@ export class MonitorReportsTask implements ReportingTask {
createTaskRunner: this.getTaskRunner(),
maxAttempts: 1,
// round the timeout value up to the nearest second, since Task Manager
- // doesn't support milliseconds
+ // doesn't support milliseconds or > 1s
timeout: Math.ceil(this.timeout.asSeconds()) + 's',
};
}
- // reschedule the task with TM and update the report document status to "Pending"
+ // reschedule the task with TM
private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) {
if (!this.taskManagerStart) {
throw new Error('Reporting task runner has not been initialized!');
}
- logger.info(`Rescheduling ${task.id} to retry after timeout expiration.`);
-
- const store = await this.getStore();
-
- const oldTaskInstance: ReportingExecuteTaskInstance = {
- taskType: REPORTING_EXECUTE_TYPE, // schedule a task to EXECUTE
- state: {},
- params: task,
- };
-
- const [report, newTask] = await Promise.all([
- await store.findReportFromTask(task),
- await this.taskManagerStart.schedule(oldTaskInstance),
- ]);
-
- await store.setReportPending(report);
+ logger.info(`Rescheduling task:${task.id} to retry.`);
+ const newTask = await this.reporting.scheduleTask(task);
return newTask;
}
diff --git a/x-pack/plugins/rollup/public/crud_app/_crud_app.scss b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
index 9e3bd491115ce..ddf69167145f1 100644
--- a/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
+++ b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
@@ -4,11 +4,3 @@
.rollupJobWizardStepActions {
align-items: flex-end; /* 1 */
}
-
-/**
- * 1. Ensure panel fills width of parent when search input yields no matching rollup jobs.
- */
-.rollupJobsListPanel {
- // sass-lint:disable-block no-important
- flex-grow: 1 !important; /* 1 */
-}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
index fa3ce260424f2..6f22345dc1cec 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { cloneDeep, debounce, first, mapValues } from 'lodash';
@@ -18,11 +18,10 @@ import {
EuiCallOut,
EuiLoadingKibana,
EuiOverlayMask,
- EuiPageContent,
- EuiPageContentHeader,
+ EuiPageContentBody,
+ EuiPageHeader,
EuiSpacer,
EuiStepsHorizontal,
- EuiTitle,
} from '@elastic/eui';
import {
@@ -522,44 +521,46 @@ export class JobCreateUi extends Component {
}
saveErrorFeedback = (
-
+ <>
+
+
{errorBody}
-
+ >
);
}
return (
-
-
-
-
-
-
-
-
-
-
- {saveErrorFeedback}
-
-
+
+
+ }
+ />
-
+
+
+
+
+ {saveErrorFeedback}
+
+
+
+ {this.renderCurrentStep()}
- {this.renderCurrentStep()}
+
-
+ {this.renderNavigation()}
- {this.renderNavigation()}
-
{savingFeedback}
-
+
);
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
index 4fe1674e8c643..5e97ff5e2980d 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
@@ -195,7 +195,7 @@ export class DetailPanel extends Component {
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
index 16919b8388e2e..e1f9ec2b3a315 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
@@ -70,7 +70,7 @@ describe(' ', () => {
({ component, find, exists } = initTestBed({ isLoading: true }));
const loading = find('rollupJobDetailLoading');
expect(loading.length).toBeTruthy();
- expect(loading.text()).toEqual('Loading rollup job...');
+ expect(loading.text()).toEqual('Loading rollup job…');
// Make sure the title and the tabs are visible
expect(exists('detailPanelTabSelected')).toBeTruthy();
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
index 589546a11ef38..b2448eb610774 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
@@ -12,24 +12,19 @@ import { i18n } from '@kbn/i18n';
import {
EuiButton,
+ EuiButtonEmpty,
EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
+ EuiPageHeader,
EuiPageContent,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
EuiSpacer,
- EuiText,
- EuiTextColor,
- EuiTitle,
- EuiCallOut,
} from '@elastic/eui';
import { withKibana } from '../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { getRouterLinkProps, listBreadcrumb } from '../../services';
+import { documentationLinks } from '../../services/documentation_links';
+
import { JobTable } from './job_table';
import { DetailPanel } from './detail_panel';
@@ -87,38 +82,26 @@ export class JobListUi extends Component {
this.props.closeDetailPanel();
}
- getHeaderSection() {
- return (
-
-
-
-
-
-
-
- );
- }
-
renderNoPermission() {
const title = i18n.translate('xpack.rollupJobs.jobList.noPermissionTitle', {
defaultMessage: 'Permission error',
});
return (
-
- {this.getHeaderSection()}
-
-
+
-
-
-
+ iconType="alert"
+ title={{title} }
+ body={
+
+
+
+ }
+ />
+
);
}
@@ -130,101 +113,110 @@ export class JobListUi extends Component {
const title = i18n.translate('xpack.rollupJobs.jobList.loadingErrorTitle', {
defaultMessage: 'Error loading rollup jobs',
});
+
return (
-
- {this.getHeaderSection()}
-
-
- {statusCode} {errorString}
-
-
+
+ {title}}
+ body={
+
+ {statusCode} {errorString}
+
+ }
+ />
+
);
}
renderEmpty() {
return (
-
-
-
- }
- body={
-
-
+
+
+
+ }
+ body={
+
+
+
+
+
+ }
+ actions={
+
+
-
-
- }
- actions={
-
-
-
- }
- />
+
+ }
+ />
+
);
}
renderLoading() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
renderList() {
- const { isLoading } = this.props;
-
return (
-
-
- {this.getHeaderSection()}
-
-
-
+ <>
+
+
+
+ }
+ rightSideItems={[
+
-
-
-
+ ,
+ ]}
+ />
- {isLoading ? this.renderLoading() : }
+
+
+
-
+ >
);
}
@@ -241,15 +233,13 @@ export class JobListUi extends Component {
}
} else if (!isLoading && !hasJobs) {
content = this.renderEmpty();
+ } else if (isLoading) {
+ content = this.renderLoading();
} else {
content = this.renderList();
}
- return (
-
- {content}
-
- );
+ return content;
}
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
index 3283f4f521fc0..b2c738a033b3c 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
@@ -22,6 +22,15 @@ jest.mock('../../services', () => {
};
});
+jest.mock('../../services/documentation_links', () => {
+ const coreMocks = jest.requireActual('../../../../../../../src/core/public/mocks');
+
+ return {
+ init: jest.fn(),
+ documentationLinks: coreMocks.docLinksServiceMock.createStartContract().links,
+ };
+});
+
const defaultProps = {
history: { location: {} },
loadJobs: () => {},
@@ -52,14 +61,14 @@ describe(' ', () => {
it('should display a loading message when loading the jobs', () => {
const { component, exists } = initTestBed({ isLoading: true });
- expect(exists('jobListLoading')).toBeTruthy();
+ expect(exists('sectionLoading')).toBeTruthy();
expect(component.find('JobTable').length).toBeFalsy();
});
it('should display the when there are jobs', () => {
const { component, exists } = initTestBed({ hasJobs: true });
- expect(exists('jobListLoading')).toBeFalsy();
+ expect(exists('sectionLoading')).toBeFalsy();
expect(component.find('JobTable').length).toBeTruthy();
});
@@ -71,21 +80,20 @@ describe(' ', () => {
},
});
- it('should display a callout with the status and the message', () => {
+ it('should display an error with the status and the message', () => {
expect(exists('jobListError')).toBeTruthy();
expect(find('jobListError').find('EuiText').text()).toEqual('400 Houston we got a problem.');
});
});
describe('when the user does not have the permission to access it', () => {
- const { exists } = initTestBed({ jobLoadError: { status: 403 } });
+ const { exists, find } = initTestBed({ jobLoadError: { status: 403 } });
- it('should render a callout message', () => {
+ it('should render an error message', () => {
expect(exists('jobListNoPermission')).toBeTruthy();
- });
-
- it('should display the page header', () => {
- expect(exists('jobListPageHeader')).toBeTruthy();
+ expect(find('jobListNoPermission').find('EuiText').text()).toEqual(
+ 'You do not have permission to view or add rollup jobs.'
+ );
});
});
});
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
index fe3d2cbd4cbe0..83135cf219f35 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -28,10 +28,11 @@ import {
EuiTableRowCellCheckbox,
EuiText,
EuiToolTip,
+ EuiButton,
} from '@elastic/eui';
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../common';
-import { METRIC_TYPE } from '../../../services';
+import { METRIC_TYPE, getRouterLinkProps } from '../../../services';
import { trackUiMetric } from '../../../../kibana_services';
import { JobActionMenu, JobStatus } from '../../components';
@@ -346,9 +347,9 @@ export class JobTable extends Component {
const atLeastOneItemSelected = Object.keys(idToSelectedJobMap).length > 0;
return (
-
-
- {atLeastOneItemSelected ? (
+
+
+ {atLeastOneItemSelected && (
- ) : null}
+ )}
+
+
+
+
+
@@ -409,7 +418,7 @@ export class JobTable extends Component {
{jobs.length > 0 ? this.renderPager() : null}
-
+
);
}
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
index 3fa879923c40a..d52f3fa35a544 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
@@ -20,6 +20,14 @@ jest.mock('../../../../kibana_services', () => {
};
});
+jest.mock('../../../services', () => {
+ const services = jest.requireActual('../../../services');
+ return {
+ ...services,
+ getRouterLinkProps: (link) => ({ href: link }),
+ };
+});
+
const defaultProps = {
jobs: [],
pager: new Pager(20, 10, 1),
diff --git a/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
index 0dc3a02d3c077..c63d01f3c200d 100644
--- a/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
+++ b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
@@ -5,9 +5,7 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
-
-import { loadJobs as sendLoadJobsRequest, deserializeJobs, showApiError } from '../../services';
+import { loadJobs as sendLoadJobsRequest, deserializeJobs } from '../../services';
import { LOAD_JOBS_START, LOAD_JOBS_SUCCESS, LOAD_JOBS_FAILURE } from '../action_types';
export const loadJobs = () => async (dispatch) => {
@@ -19,17 +17,10 @@ export const loadJobs = () => async (dispatch) => {
try {
jobs = await sendLoadJobsRequest();
} catch (error) {
- dispatch({
+ return dispatch({
type: LOAD_JOBS_FAILURE,
payload: { error },
});
-
- return showApiError(
- error,
- i18n.translate('xpack.rollupJobs.loadAction.errorTitle', {
- defaultMessage: 'Error loading rollup jobs',
- })
- );
}
dispatch({
diff --git a/x-pack/plugins/rollup/public/shared_imports.ts b/x-pack/plugins/rollup/public/shared_imports.ts
index fd28175318666..c8d7f1d9f13f3 100644
--- a/x-pack/plugins/rollup/public/shared_imports.ts
+++ b/x-pack/plugins/rollup/public/shared_imports.ts
@@ -5,4 +5,8 @@
* 2.0.
*/
-export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public';
+export {
+ extractQueryParams,
+ indices,
+ SectionLoading,
+} from '../../../../src/plugins/es_ui_shared/public';
diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
index fa1a786bc8a71..46ddfbcfc2de5 100644
--- a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
+++ b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
@@ -5,10 +5,10 @@
* 2.0.
*/
-import { getRouter, setHttp } from '../../crud_app/services';
+import { getRouter, setHttp, init as initDocumentation } from '../../crud_app/services';
import { mockHttpRequest, pageHelpers, nextTick } from './helpers';
import { JOBS } from './helpers/constants';
-import { coreMock } from '../../../../../../src/core/public/mocks';
+import { coreMock, docLinksServiceMock } from '../../../../../../src/core/public/mocks';
jest.mock('../../crud_app/services', () => {
const services = jest.requireActual('../../crud_app/services');
@@ -38,6 +38,7 @@ describe(' ', () => {
beforeAll(() => {
startMock = coreMock.createStart();
setHttp(startMock.http);
+ initDocumentation(docLinksServiceMock.createStartContract());
});
beforeEach(async () => {
diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
index cfb63893ee423..3987e18538e57 100644
--- a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
+++ b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
@@ -24,6 +24,15 @@ jest.mock('../../kibana_services', () => {
};
});
+jest.mock('../../crud_app/services/documentation_links', () => {
+ const coreMocks = jest.requireActual('../../../../../../src/core/public/mocks');
+
+ return {
+ init: jest.fn(),
+ documentationLinks: coreMocks.docLinksServiceMock.createStartContract().links,
+ };
+});
+
const { setup } = pageHelpers.jobList;
describe('Smoke test cloning an existing rollup job from job list', () => {
diff --git a/x-pack/plugins/security/common/model/user.ts b/x-pack/plugins/security/common/model/user.ts
index 3f4787b207f88..2bcea659699cb 100644
--- a/x-pack/plugins/security/common/model/user.ts
+++ b/x-pack/plugins/security/common/model/user.ts
@@ -7,8 +7,8 @@
export interface User {
username: string;
- email: string;
- full_name: string;
+ email?: string;
+ full_name?: string;
roles: readonly string[];
enabled: boolean;
metadata?: {
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx
index 749c1c8ccb4e2..48a0d18653053 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx
@@ -12,7 +12,6 @@ import {
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
- EuiOverlayMask,
EuiTitle,
} from '@elastic/eui';
import React, { Fragment, useState } from 'react';
@@ -47,37 +46,39 @@ export const PrivilegeSummary = (props: Props) => {
/>
{isOpen && (
-
- setIsOpen(false)} size={flyoutSize}>
-
-
-
-
-
-
-
-
-
-
-
- setIsOpen(false)}>
+ setIsOpen(false)}
+ size={flyoutSize}
+ maskProps={{ headerZindexLocation: 'below' }}
+ >
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+ setIsOpen(false)}>
+
+
+
+
)}
);
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
index b290cb301866d..8f62acd463e6a 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
@@ -20,7 +20,6 @@ import {
EuiFlyoutHeader,
EuiForm,
EuiFormRow,
- EuiOverlayMask,
EuiSpacer,
EuiText,
EuiTitle,
@@ -93,64 +92,67 @@ export class PrivilegeSpaceForm extends Component {
public render() {
return (
-
-
-
-
-
-
-
-
-
-
- {this.getForm()}
-
-
- {this.state.privilegeCalculator.hasSupersededInheritedPrivileges(
- this.state.privilegeIndex
- ) && (
-
-
- }
- >
-
-
-
-
- )}
-
-
-
+
+
+
+
+
+
+
+
+
+ {this.getForm()}
+
+
+ {this.state.privilegeCalculator.hasSupersededInheritedPrivileges(
+ this.state.privilegeIndex
+ ) && (
+
+
-
-
- {this.getSaveButton()}
-
-
-
-
+ }
+ >
+
+
+
+
+ )}
+
+
+
+
+
+
+ {this.getSaveButton()}
+
+
+
);
}
diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap
index eb266ce93338c..f36a1bf477b06 100644
--- a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap
+++ b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap
@@ -58,33 +58,33 @@ exports[` renders permission denied if required 1`] = `
/>
+
+
+
+ You need permission to manage roles
+
+
+
-
-
-
- You need permission to manage roles
-
-
-
diff --git a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx
index 29d87e31797cc..8101c09d64907 100644
--- a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx
+++ b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx
@@ -41,8 +41,8 @@ export const THROTTLE_USERS_WAIT = 10000;
export interface UserFormValues {
username?: string;
- full_name: string;
- email: string;
+ full_name?: string;
+ email?: string;
password?: string;
confirm_password?: string;
roles: readonly string[];
diff --git a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap
index bcb97538b4f05..2ee2337fc9aeb 100644
--- a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap
+++ b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`PromptPage renders as expected with additional scripts 1`] = `"Elastic MockedFonts "`;
+exports[`PromptPage renders as expected with additional scripts 1`] = `"Elastic MockedFonts "`;
-exports[`PromptPage renders as expected without additional scripts 1`] = `"Elastic MockedFonts "`;
+exports[`PromptPage renders as expected without additional scripts 1`] = `"Elastic MockedFonts "`;
diff --git a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap
index 55168401992f7..2e7f3d49e478f 100644
--- a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap
+++ b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`UnauthenticatedPage renders as expected 1`] = `"Elastic MockedFonts
We couldn't log you in
We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator.
"`;
+exports[`UnauthenticatedPage renders as expected 1`] = `"Elastic MockedFonts
We couldn't log you in
We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator.
"`;
diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts
index 4cbca1c70f507..1707ca710aaf8 100644
--- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts
+++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts
@@ -224,7 +224,7 @@ export class APIKeys {
try {
result = (
await this.clusterClient.asInternalUser.security.grantApiKey({
- // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors
+ // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors doesn't support `Record`
body: params,
})
).body;
diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts
index c7c0edcf1e9e1..f6d9af24ee1ad 100644
--- a/x-pack/plugins/security/server/authentication/providers/base.ts
+++ b/x-pack/plugins/security/server/authentication/providers/base.ts
@@ -117,7 +117,7 @@ export abstract class BaseAuthenticationProvider {
*/
protected async getUser(request: KibanaRequest, authHeaders: Headers = {}) {
return this.authenticationInfoToAuthenticatedUser(
- // @ts-expect-error @elastic/elasticsearch `AuthenticateResponse` type doesn't define `authentication_type` and `enabled`.
+ // @ts-expect-error Metadata is defined as Record
(
await this.options.client
.asScoped({ headers: { ...request.headers, ...authHeaders } })
diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts
index 43338a8f6400f..fae0d7ca69038 100644
--- a/x-pack/plugins/security/server/authentication/providers/token.ts
+++ b/x-pack/plugins/security/server/authentication/providers/token.ts
@@ -84,7 +84,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug('Get token API request to Elasticsearch successful');
return AuthenticationResult.succeeded(
this.authenticationInfoToAuthenticatedUser(
- // @ts-expect-error @elastic/elasticsearch GetUserAccessTokenResponse declares authentication: string, but expected AuthenticatedUser
+ // @ts-expect-error @elastic/elasticsearch metadata defined as Record;
authenticationInfo as AuthenticationInfo
),
{
diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts
index 1adbb2dc66533..47051cc08da23 100644
--- a/x-pack/plugins/security/server/authentication/tokens.ts
+++ b/x-pack/plugins/security/server/authentication/tokens.ts
@@ -73,7 +73,7 @@ export class Tokens {
return {
accessToken,
refreshToken,
- // @ts-expect-error @elastic/elasticsearch declared GetUserAccessTokenResponse.authentication: string
+ // @ts-expect-error @elastic/elasticsearch user metadata defined as Record
authenticationInfo: authenticationInfo as AuthenticationInfo,
};
} catch (err) {
diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap
index 1011d82eb1f73..8d31770cd9385 100644
--- a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap
+++ b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ResetSessionPage renders as expected 1`] = `"Elastic MockedFonts
You do not have permission to access the requested page
Either go back to the previous page or log in as a different user.
"`;
+exports[`ResetSessionPage renders as expected 1`] = `"Elastic MockedFonts
You do not have permission to access the requested page
Either go back to the previous page or log in as a different user.
"`;
diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts
index 01d32f7fb8233..075a8d133f1e6 100644
--- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts
+++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts
@@ -32,7 +32,7 @@ export function defineGetRolesRoutes({ router, authz }: RouteDefinitionParams) {
if (elasticsearchRole) {
return response.ok({
body: transformElasticsearchRoleToRole(
- // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`.
+ // @ts-expect-error `SecurityIndicesPrivileges.names` expected to be `string[]`
elasticsearchRole,
request.params.name,
authz.applicationName
diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts
index 4d458be4e332f..be0880a06d59d 100644
--- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts
+++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts
@@ -27,7 +27,7 @@ export function defineGetAllRolesRoutes({ router, authz }: RouteDefinitionParams
body: Object.entries(elasticsearchRoles)
.map(([roleName, elasticsearchRole]) =>
transformElasticsearchRoleToRole(
- // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`.
+ // @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[]
elasticsearchRole,
roleName,
authz.applicationName
diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts
index 67cd8975b76eb..257b4210b13f7 100644
--- a/x-pack/plugins/security/server/routes/role_mapping/get.ts
+++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts
@@ -36,7 +36,7 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) {
return {
name,
...mapping,
- // @ts-expect-error @elastic/elasticsearch `XPackRoleMapping` type doesn't define `role_templates` property.
+ // @ts-expect-error @elastic/elasticsearch `SecurityRoleMapping` doeesn't contain `role_templates`
role_templates: (mapping.role_templates || []).map((entry: RoleTemplate) => {
return {
...entry,
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 4ce20af28b1d7..d59d7e7b7da4f 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -44,7 +44,8 @@ export const DEFAULT_INTERVAL_VALUE = 300000; // ms
export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges';
export const DEFAULT_TRANSFORMS = 'securitySolution:transforms';
export const SCROLLING_DISABLED_CLASS_NAME = 'scrolling-disabled';
-export const GLOBAL_HEADER_HEIGHT = 98; // px
+export const GLOBAL_HEADER_HEIGHT = 96; // px
+export const GLOBAL_HEADER_HEIGHT_WITH_GLOBAL_BANNER = 128; // px
export const FILTERS_GLOBAL_HEIGHT = 109; // px
export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled';
export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51';
@@ -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`;
@@ -237,6 +241,7 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [
'.email',
'.slack',
'.pagerduty',
+ '.swimlane',
'.webhook',
'.servicenow',
'.jira',
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
index 99753242e7627..dfaad68e295eb 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
@@ -58,7 +58,6 @@ export interface ActivityLogActionResponse {
}
export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse;
export interface ActivityLog {
- total: number;
page: number;
pageSize: number;
data: ActivityLogEntry[];
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index b20b1501eecc5..a9a81aa285af7 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -15,6 +15,7 @@ const allowedExperimentalValues = Object.freeze({
trustedAppsByPolicyEnabled: false,
metricsEntitiesEnabled: false,
ruleRegistryEnabled: false,
+ tGridEnabled: false,
});
type ExperimentalConfigKeys = Array;
diff --git a/x-pack/plugins/security_solution/common/index.ts b/x-pack/plugins/security_solution/common/index.ts
index 1fec1c76430eb..e6d7bcc9bd506 100644
--- a/x-pack/plugins/security_solution/common/index.ts
+++ b/x-pack/plugins/security_solution/common/index.ts
@@ -4,3 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
+export * from './types';
+export * from './search_strategy';
+export * from './utility_types';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
index 4fcfbdac3c1b4..095ba4ca20afc 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
@@ -4,52 +4,27 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import type { estypes } from '@elastic/elasticsearch';
import { IEsSearchResponse } from '../../../../../../src/plugins/data/common';
+export type {
+ Inspect,
+ SortField,
+ TimerangeInput,
+ PaginationInputPaginated,
+ DocValueFields,
+ CursorType,
+ TotalValue,
+} from '../../../../timelines/common';
+export { Direction } from '../../../../timelines/common';
export type Maybe = T | null;
export type SearchHit = IEsSearchResponse['rawResponse']['hits']['hits'][0];
-export interface TotalValue {
- value: number;
- relation: string;
-}
-
-export interface Inspect {
- dsl: string[];
-}
-
export interface PageInfoPaginated {
activePage: number;
fakeTotalCount: number;
showMorePagesIndicator: boolean;
}
-
-export interface CursorType {
- value?: Maybe;
- tiebreaker?: Maybe;
-}
-
-export enum Direction {
- asc = 'asc',
- desc = 'desc',
-}
-
-export interface SortField {
- field: Field;
- direction: Direction;
-}
-
-export interface TimerangeInput {
- /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */
- interval: string;
- /** The end of the timerange */
- to: string;
- /** The beginning of the timerange */
- from: string;
-}
-
export interface PaginationInput {
/** The limit parameter allows you to configure the maximum amount of items to be returned */
limit: number;
@@ -59,19 +34,6 @@ export interface PaginationInput {
tiebreaker?: Maybe;
}
-export interface PaginationInputPaginated {
- /** The activePage parameter defines the page of results you want to fetch */
- activePage: number;
- /** The cursorStart parameter defines the start of the results to be displayed */
- cursorStart: number;
- /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */
- fakePossibleCount: number;
- /** The querySize parameter is the number of items to be returned */
- querySize: number;
-}
-
-export type DocValueFields = estypes.SearchDocValueField;
-
export interface Explanation {
value: number;
description: string;
@@ -111,13 +73,3 @@ export interface GenericBuckets {
}
export type StringOrNumber = string | number;
-
-export interface TimerangeFilter {
- range: {
- [timestamp: string]: {
- gte: string;
- lte: string;
- format: string;
- };
- };
-}
diff --git a/x-pack/plugins/security_solution/common/search_strategy/index.ts b/x-pack/plugins/security_solution/common/search_strategy/index.ts
index 575256b991d16..e3d6736878063 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/index.ts
@@ -8,3 +8,4 @@
export * from './common';
export * from './security_solution';
export * from './timeline';
+export * from './index_fields';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts
index d747758640fab..4e5f8af41a2ef 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts
@@ -5,37 +5,10 @@
* 2.0.
*/
-import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
-import { Ecs } from '../../../../ecs';
-import { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common';
-import { TimelineRequestOptionsPaginated } from '../..';
-
-export interface TimelineEdges {
- node: TimelineItem;
- cursor: CursorType;
-}
-
-export interface TimelineItem {
- _id: string;
- _index?: Maybe;
- data: TimelineNonEcsData[];
- ecs: Ecs;
-}
-
-export interface TimelineNonEcsData {
- field: string;
- value?: Maybe;
-}
-
-export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse {
- edges: TimelineEdges[];
- totalCount: number;
- pageInfo: Pick;
- inspect?: Maybe;
-}
-
-export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsPaginated {
- fields: string[] | Array<{ field: string; include_unmapped: boolean }>;
- fieldRequested: string[];
- language: 'eql' | 'kuery' | 'lucene';
-}
+export type {
+ TimelineEdges,
+ TimelineItem,
+ TimelineNonEcsData,
+ TimelineEventsAllStrategyResponse,
+ TimelineEventsAllRequestOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts
index 4a5bd2c99a0eb..e4d2ea52ffdff 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts
@@ -5,22 +5,8 @@
* 2.0.
*/
-import { Ecs } from '../../../../ecs';
-import { CursorType, Maybe } from '../../../common';
-
-export interface TimelineEdges {
- node: TimelineItem;
- cursor: CursorType;
-}
-
-export interface TimelineItem {
- _id: string;
- _index?: Maybe;
- data: TimelineNonEcsData[];
- ecs: Ecs;
-}
-
-export interface TimelineNonEcsData {
- field: string;
- value?: Maybe;
-}
+export type {
+ TimelineEdges,
+ TimelineItem,
+ TimelineNonEcsData,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
index 1f9820f8e5c2b..3fd13e56cc7e7 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
@@ -5,27 +5,8 @@
* 2.0.
*/
-import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
-import { Inspect, Maybe } from '../../../common';
-import { TimelineRequestOptionsPaginated } from '../..';
-
-export interface TimelineEventsDetailsItem {
- ariaRowindex?: Maybe;
- category?: string;
- field: string;
- values?: Maybe;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- originalValue?: Maybe;
- isObjectArray: boolean;
-}
-
-export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse {
- data?: Maybe;
- inspect?: Maybe;
-}
-
-export interface TimelineEventsDetailsRequestOptions
- extends Partial {
- indexName: string;
- eventId: string;
-}
+export type {
+ TimelineEventsDetailsItem,
+ TimelineEventsDetailsStrategyResponse,
+ TimelineEventsDetailsRequestOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts
index c508876032fca..10e9bbd7670cd 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts
@@ -5,43 +5,10 @@
* 2.0.
*/
-import { EuiComboBoxOptionOption } from '@elastic/eui';
-import {
- EqlSearchStrategyRequest,
- EqlSearchStrategyResponse,
-} from '../../../../../../../../src/plugins/data/common';
-import { Inspect, Maybe, PaginationInputPaginated } from '../../..';
-import { TimelineEdges, TimelineEventsAllRequestOptions } from '../..';
-import { EqlSearchResponse } from '../../../../detection_engine/types';
-
-export interface TimelineEqlRequestOptions
- extends EqlSearchStrategyRequest,
- Omit {
- eventCategoryField?: string;
- tiebreakerField?: string;
- timestampField?: string;
- size?: number;
-}
-
-export interface TimelineEqlResponse extends EqlSearchStrategyResponse> {
- edges: TimelineEdges[];
- totalCount: number;
- pageInfo: Pick;
- inspect: Maybe;
-}
-
-export interface EqlOptionsData {
- keywordFields: EuiComboBoxOptionOption[];
- dateFields: EuiComboBoxOptionOption[];
- nonDateFields: EuiComboBoxOptionOption[];
-}
-
-export interface EqlOptionsSelected {
- eventCategoryField?: string;
- tiebreakerField?: string;
- timestampField?: string;
- query?: string;
- size?: number;
-}
-
-export type FieldsEqlOptions = keyof EqlOptionsSelected;
+export type {
+ TimelineEqlRequestOptions,
+ TimelineEqlResponse,
+ EqlOptionsData,
+ EqlOptionsSelected,
+ FieldsEqlOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts
index f29dc4a3c7450..39f23a63c8afe 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts
@@ -5,38 +5,11 @@
* 2.0.
*/
-import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
-import { Inspect, Maybe } from '../../../common';
-import { TimelineRequestBasicOptions } from '../..';
-
-export enum LastEventIndexKey {
- hostDetails = 'hostDetails',
- hosts = 'hosts',
- ipDetails = 'ipDetails',
- network = 'network',
-}
-
-export interface LastTimeDetails {
- hostName?: Maybe;
- ip?: Maybe;
-}
-
-export interface TimelineEventsLastEventTimeStrategyResponse extends IEsSearchResponse {
- lastSeen: Maybe;
- inspect?: Maybe;
-}
-
-export interface TimelineKpiStrategyResponse extends IEsSearchResponse {
- destinationIpCount: number;
- inspect?: Maybe;
- hostCount: number;
- processCount: number;
- sourceIpCount: number;
- userCount: number;
-}
-
-export interface TimelineEventsLastEventTimeRequestOptions
- extends Omit {
- indexKey: LastEventIndexKey;
- details: LastTimeDetails;
-}
+export { LastEventIndexKey } from '../../../../../../timelines/common';
+
+export type {
+ LastTimeDetails,
+ TimelineEventsLastEventTimeStrategyResponse,
+ TimelineKpiStrategyResponse,
+ TimelineEventsLastEventTimeRequestOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts
index 9c2c23eb334a3..7064ef033fc5a 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts
@@ -24,7 +24,12 @@ import {
SortField,
Maybe,
} from '../common';
-import { DataProviderType, TimelineType, TimelineStatus } from '../../types/timeline';
+import {
+ DataProviderType,
+ TimelineType,
+ TimelineStatus,
+ RowRendererId,
+} from '../../types/timeline';
export * from './events';
@@ -165,25 +170,6 @@ export interface SortTimelineInput {
sortDirection?: Maybe;
}
-export enum RowRendererId {
- alerts = 'alerts',
- auditd = 'auditd',
- auditd_file = 'auditd_file',
- library = 'library',
- netflow = 'netflow',
- plain = 'plain',
- registry = 'registry',
- suricata = 'suricata',
- system = 'system',
- system_dns = 'system_dns',
- system_endgame_process = 'system_endgame_process',
- system_file = 'system_file',
- system_fim = 'system_fim',
- system_security_event = 'system_security_event',
- system_socket = 'system_socket',
- zeek = 'zeek',
-}
-
export interface TimelineInput {
columns?: Maybe;
dataProviders?: Maybe;
diff --git a/x-pack/plugins/index_management/public/application/components/page_error/index.ts b/x-pack/plugins/security_solution/common/types/index.ts
similarity index 80%
rename from x-pack/plugins/index_management/public/application/components/page_error/index.ts
rename to x-pack/plugins/security_solution/common/types/index.ts
index 040edfa362c63..9464a33082a49 100644
--- a/x-pack/plugins/index_management/public/application/components/page_error/index.ts
+++ b/x-pack/plugins/security_solution/common/types/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { PageErrorForbidden } from './page_error_forbidden';
+export * from './timeline';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts b/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts
new file mode 100644
index 0000000000000..782af107417c2
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+export type {
+ ActionProps,
+ HeaderActionProps,
+ GenericActionRowCellRenderProps,
+ HeaderCellRender,
+ RowCellRender,
+ ControlColumnProps,
+} from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts
new file mode 100644
index 0000000000000..83b0ced332a62
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export type { CellValueElementProps } from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts b/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts
new file mode 100644
index 0000000000000..ee4d621e35d6c
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export type {
+ ColumnHeaderType,
+ ColumnId,
+ ColumnHeaderOptions,
+ ColumnRenderer,
+} from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts b/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts
new file mode 100644
index 0000000000000..f363176ac0a88
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+export { IS_OPERATOR, EXISTS_OPERATOR } from '../../../../../timelines/common';
+
+export type {
+ QueryOperator,
+ DataProviderType,
+ QueryMatch,
+ DataProvider,
+ DataProvidersAnd,
+} from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts
index 7ae52a3990ff7..05cf99195774b 100644
--- a/x-pack/plugins/security_solution/common/types/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts
@@ -23,6 +23,13 @@ import { FlowTarget } from '../../search_strategy/security_solution/network';
import { errorSchema } from '../../detection_engine/schemas/response/error_schema';
import { Direction, Maybe } from '../../search_strategy';
+export * from './actions';
+export * from './cells';
+export * from './columns';
+export * from './data_provider';
+export * from './rows';
+export * from './store';
+
/*
* ColumnHeader Types
*/
@@ -492,6 +499,11 @@ export type TimelineExpandedDetail = {
[tab in TimelineTabs]?: TimelineExpandedDetailType;
};
+export type ToggleDetailPanel = TimelineExpandedDetailType & {
+ tabType?: TimelineTabs;
+ timelineId: string;
+};
+
export const pageInfoTimeline = runtimeTypes.type({
pageIndex: runtimeTypes.number,
pageSize: runtimeTypes.number,
diff --git a/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts b/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts
new file mode 100644
index 0000000000000..ae2d19a5e2ca8
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+export type { RowRenderer } from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts
new file mode 100644
index 0000000000000..01fc9db7c8e1d
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ ColumnHeaderOptions,
+ ColumnId,
+ RowRendererId,
+ TimelineExpandedDetail,
+ TimelineTypeLiteral,
+} from '.';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { Filter } from '../../../../../../src/plugins/data/public';
+
+import { Direction } from '../../search_strategy';
+import { DataProvider } from './data_provider';
+
+export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql';
+
+export interface KueryFilterQuery {
+ kind: KueryFilterQueryKind;
+ expression: string;
+}
+
+export interface SerializedFilterQuery {
+ kuery: KueryFilterQuery | null;
+ serializedQuery: string;
+}
+
+export type SortDirection = 'none' | 'asc' | 'desc' | Direction;
+export interface SortColumnTimeline {
+ columnId: string;
+ columnType: string;
+ sortDirection: SortDirection;
+}
+
+export interface TimelinePersistInput {
+ id: string;
+ dataProviders?: DataProvider[];
+ dateRange?: {
+ start: string;
+ end: string;
+ };
+ excludedRowRendererIds?: RowRendererId[];
+ expandedDetail?: TimelineExpandedDetail;
+ filters?: Filter[];
+ columns: ColumnHeaderOptions[];
+ itemsPerPage?: number;
+ indexNames: string[];
+ kqlQuery?: {
+ filterQuery: SerializedFilterQuery | null;
+ };
+ show?: boolean;
+ sort?: SortColumnTimeline[];
+ showCheckboxes?: boolean;
+ timelineType?: TimelineTypeLiteral;
+ templateTimelineId?: string | null;
+ templateTimelineVersion?: number | null;
+}
+
+/** Invoked when a column is sorted */
+export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void;
+
+export type OnColumnsSorted = (
+ sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }>
+) => void;
+
+export type OnColumnRemoved = (columnId: ColumnId) => void;
+
+export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void;
+
+/** Invoked when a user clicks to load more item */
+export type OnChangePage = (nextPage: number) => void;
+
+/** Invoked when a user checks/un-checks a row */
+export type OnRowSelected = ({
+ eventIds,
+ isSelected,
+}: {
+ eventIds: string[];
+ isSelected: boolean;
+}) => void;
+
+/** Invoked when a user checks/un-checks the select all checkbox */
+export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void;
+
+/** Invoked when columns are updated */
+export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void;
+
+/** Invoked when a user pins an event */
+export type OnPinEvent = (eventId: string) => void;
+
+/** Invoked when a user unpins an event */
+export type OnUnPinEvent = (eventId: string) => void;
diff --git a/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts
index b724c0f672b50..64d4f2986903a 100644
--- a/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts
+++ b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts
@@ -7,7 +7,7 @@
import { EventHit, EventSource } from '../search_strategy';
import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './field_formatters';
-import { eventDetailsFormattedFields, eventHit } from './mock_event_details';
+import { eventDetailsFormattedFields, eventHit } from '@kbn/securitysolution-t-grid';
describe('Events Details Helpers', () => {
const fields: EventHit['fields'] = eventHit.fields;
diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts
index 7f0016e39ff88..3f3209b52120e 100644
--- a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts
@@ -19,8 +19,7 @@ import { createTimeline } from '../../tasks/api_calls/timelines';
import { cleanKibana } from '../../tasks/common';
import { createCase } from '../../tasks/api_calls/cases';
-// TODO: enable once attach timeline to cases is re-enabled
-describe.skip('attach timeline to case', () => {
+describe('attach timeline to case', () => {
context('without cases created', () => {
beforeEach(() => {
cleanKibana();
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts
index bdf2ab96600ea..932f1ceac61e8 100644
--- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts
@@ -44,7 +44,7 @@ describe('Alerts timeline', () => {
});
it('should not allow user with read only privileges to attach alerts to cases', () => {
- cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('be.disabled');
+ cy.get(ATTACH_ALERT_TO_CASE_BUTTON).should('not.exist');
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts
index f1ee0d39f545f..bf5c281a43e39 100644
--- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts
@@ -129,7 +129,13 @@ describe('Alerts detection rules', () => {
});
it('Auto refreshes rules', () => {
- cy.clock(Date.now());
+ /**
+ * Ran into the error: timer created with setInterval() but cleared with cancelAnimationFrame()
+ * There are no cancelAnimationFrames in the codebase that are used to clear a setInterval so
+ * explicitly set the below overrides. see https://docs.cypress.io/api/commands/clock#Function-names
+ */
+
+ cy.clock(Date.now(), ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date']);
goToManageAlertsDetectionRules();
waitForRulesTableToBeLoaded();
diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
index dc5b247e3ec43..3ff036fa0107f 100644
--- a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
@@ -15,6 +15,8 @@ import { OVERVIEW_URL } from '../../urls/navigation';
import overviewFixture from '../../fixtures/overview_search_strategy.json';
import emptyInstance from '../../fixtures/empty_instance.json';
import { cleanKibana } from '../../tasks/common';
+import { createTimeline, favoriteTimeline } from '../../tasks/api_calls/timelines';
+import { timeline } from '../../objects/timeline';
describe('Overview Page', () => {
before(() => {
@@ -43,9 +45,26 @@ describe('Overview Page', () => {
describe('with no data', () => {
it('Splash screen should be here', () => {
- cy.stubSearchStrategyApi(emptyInstance, undefined, 'securitySolutionIndexFields');
+ cy.stubSearchStrategyApi(emptyInstance, undefined, 'indexFields');
loginAndWaitForPage(OVERVIEW_URL);
cy.get(OVERVIEW_EMPTY_PAGE).should('be.visible');
});
});
+
+ describe('Favorite Timelines', () => {
+ it('should appear on overview page', () => {
+ createTimeline(timeline)
+ .then((response) => response.body.data.persistTimeline.timeline.savedObjectId)
+ .then((timelineId: string) => {
+ favoriteTimeline({ timelineId, timelineType: 'default' }).then(() => {
+ cy.stubSearchStrategyApi(overviewFixture, 'overviewNetwork');
+ loginAndWaitForPage(OVERVIEW_URL);
+ cy.get('[data-test-subj="overview-recent-timelines"]').should(
+ 'contain',
+ timeline.title
+ );
+ });
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts
index a600b5edfd632..e2c1d7eef38c3 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts
@@ -16,6 +16,7 @@ import {
NOTES_TEXT_AREA,
PIN_EVENT,
TIMELINE_DESCRIPTION,
+ TIMELINE_FLYOUT_WRAPPER,
TIMELINE_QUERY,
TIMELINE_TITLE,
} from '../../screens/timeline';
@@ -25,34 +26,38 @@ import {
TIMELINES_NOTES_COUNT,
TIMELINES_FAVORITE,
} from '../../screens/timelines';
+import { createTimeline } from '../../tasks/api_calls/timelines';
import { cleanKibana } from '../../tasks/common';
-import { loginAndWaitForPage } from '../../tasks/login';
+import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { openTimelineUsingToggle } from '../../tasks/security_main';
import {
addDescriptionToTimeline,
addFilter,
addNameToTimeline,
addNotesToTimeline,
+ clickingOnCreateTemplateFromTimelineBtn,
closeTimeline,
createNewTimelineTemplate,
+ expandEventAction,
markAsFavorite,
openTimelineTemplateFromSettings,
populateTimeline,
waitForTimelineChanges,
} from '../../tasks/timeline';
-import { openTimeline } from '../../tasks/timelines';
+import { openTimeline, waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines';
-import { OVERVIEW_URL } from '../../urls/navigation';
+import { TIMELINES_URL } from '../../urls/navigation';
describe('Timeline Templates', () => {
beforeEach(() => {
cleanKibana();
+ loginAndWaitForPageWithoutDateRange(TIMELINES_URL);
+
cy.intercept('PATCH', '/api/timeline').as('timeline');
});
it('Creates a timeline template', async () => {
- loginAndWaitForPage(OVERVIEW_URL);
openTimelineUsingToggle();
createNewTimelineTemplate();
populateTimeline();
@@ -97,4 +102,22 @@ describe('Timeline Templates', () => {
cy.get(NOTES).should('have.text', timeline.notes);
});
});
+
+ it('Create template from timeline', () => {
+ waitForTimelinesPanelToBeLoaded();
+
+ createTimeline(timeline).then(() => {
+ expandEventAction();
+ clickingOnCreateTemplateFromTimelineBtn();
+ cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => {
+ expect(request.body.timeline).to.haveOwnProperty('templateTimelineId');
+ expect(request.body.timeline).to.haveOwnProperty('description', timeline.description);
+ expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty(
+ 'expression',
+ timeline.query
+ );
+ cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible');
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts
index b08bae26bf7ed..8a90b67682cb2 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts
@@ -8,32 +8,37 @@
import { timeline } from '../../objects/timeline';
import {
- FAVORITE_TIMELINE,
LOCKED_ICON,
NOTES_TEXT,
PIN_EVENT,
+ SERVER_SIDE_EVENT_COUNT,
TIMELINE_FILTER,
+ TIMELINE_FLYOUT_WRAPPER,
TIMELINE_PANEL,
+ TIMELINE_TAB_CONTENT_EQL,
} from '../../screens/timeline';
+import { createTimelineTemplate } from '../../tasks/api_calls/timelines';
import { cleanKibana } from '../../tasks/common';
-import { loginAndWaitForPage } from '../../tasks/login';
+import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { openTimelineUsingToggle } from '../../tasks/security_main';
import {
+ addEqlToTimeline,
addFilter,
addNameAndDescriptionToTimeline,
addNotesToTimeline,
+ clickingOnCreateTimelineFormTemplateBtn,
closeTimeline,
createNewTimeline,
+ expandEventAction,
goToQueryTab,
- markAsFavorite,
pinFirstEvent,
populateTimeline,
- waitForTimelineChanges,
} from '../../tasks/timeline';
-import { OVERVIEW_URL } from '../../urls/navigation';
+import { OVERVIEW_URL, TIMELINE_TEMPLATES_URL } from '../../urls/navigation';
+import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines';
describe('Timelines', (): void => {
before(() => {
@@ -88,10 +93,44 @@ describe('Timelines', (): void => {
cy.get(NOTES_TEXT).should('have.text', timeline.notes);
});
- it('can be marked as favorite', () => {
- markAsFavorite();
- waitForTimelineChanges();
- cy.get(FAVORITE_TIMELINE).should('have.text', 'Remove from favorites');
+ it('should update timeline after adding eql', () => {
+ cy.intercept('PATCH', '/api/timeline').as('updateTimeline');
+ const eql = 'any where process.name == "which"';
+ addEqlToTimeline(eql);
+
+ cy.wait('@updateTimeline', { timeout: 10000 }).its('response.statusCode').should('eq', 200);
+
+ cy.get(`${TIMELINE_TAB_CONTENT_EQL} ${SERVER_SIDE_EVENT_COUNT}`)
+ .invoke('text')
+ .then(parseInt)
+ .should('be.gt', 0);
+ });
+ });
+});
+
+describe('Create a timeline from a template', () => {
+ before(() => {
+ cleanKibana();
+ loginAndWaitForPageWithoutDateRange(TIMELINE_TEMPLATES_URL);
+ waitForTimelinesPanelToBeLoaded();
+ });
+
+ it('Should have the same query and open the timeline modal', () => {
+ createTimelineTemplate(timeline).then(() => {
+ expandEventAction();
+ cy.intercept('/api/timeline').as('timeline');
+
+ clickingOnCreateTimelineFormTemplateBtn();
+ cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => {
+ if (request.body && request.body.timeline) {
+ expect(request.body.timeline).to.haveOwnProperty('description', timeline.description);
+ expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty(
+ 'expression',
+ timeline.query
+ );
+ cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible');
+ }
+ });
});
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts
index d42632a66eb26..a89ddf3e0b250 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts
@@ -12,6 +12,7 @@ import {
TIMELINE_DATA_PROVIDERS_ACTION_MENU,
IS_DRAGGING_DATA_PROVIDERS,
TIMELINE_FLYOUT_HEADER,
+ TIMELINE_FLYOUT,
} from '../../screens/timeline';
import { HOSTS_NAMES_DRAGGABLE } from '../../screens/hosts/all_hosts';
@@ -46,7 +47,7 @@ describe('timeline data providers', () => {
it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => {
dragAndDropFirstHostToTimeline();
openTimelineUsingToggle();
- cy.get(TIMELINE_DROPPED_DATA_PROVIDERS)
+ cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_DROPPED_DATA_PROVIDERS}`)
.first()
.invoke('text')
.then((dataProviderText) => {
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts
index c7ec17d027e80..38c6f41f1049c 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts
@@ -61,8 +61,10 @@ describe('timeline flyout button', () => {
it('the `(+)` button popover menu owns focus', () => {
cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true });
- cy.get(CREATE_NEW_TIMELINE).should('have.focus');
- cy.get('body').type('{esc}');
+ cy.get(`${CREATE_NEW_TIMELINE}`)
+ .pipe(($el) => $el.trigger('focus'))
+ .should('have.focus');
+ cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').type('{esc}');
cy.get(CREATE_NEW_TIMELINE).should('not.be.visible');
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts
new file mode 100644
index 0000000000000..9cd3b22fc2bb4
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { TIMELINE_HEADER, TIMELINE_TABS } from '../../screens/timeline';
+import { cleanKibana } from '../../tasks/common';
+
+import { loginAndWaitForPage } from '../../tasks/login';
+import {
+ openTimelineUsingToggle,
+ enterFullScreenMode,
+ exitFullScreenMode,
+} from '../../tasks/security_main';
+import { populateTimeline } from '../../tasks/timeline';
+
+import { HOSTS_URL } from '../../urls/navigation';
+
+describe('Toggle full screen', () => {
+ before(() => {
+ cleanKibana();
+ loginAndWaitForPage(HOSTS_URL);
+ openTimelineUsingToggle();
+ populateTimeline();
+ });
+
+ it('Should hide timeline header and tab list area', () => {
+ enterFullScreenMode();
+
+ cy.get(TIMELINE_TABS).should('not.exist');
+ cy.get(TIMELINE_HEADER).should('not.be.visible');
+ });
+
+ it('Should show timeline header and tab list area', () => {
+ exitFullScreenMode();
+ cy.get(TIMELINE_TABS).should('exist');
+ cy.get(TIMELINE_HEADER).should('be.visible');
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts
index 2505930f72f82..24309b8fda084 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts
@@ -7,7 +7,13 @@
import { timelineNonValidQuery } from '../../objects/timeline';
-import { NOTES_TEXT, NOTES_TEXT_AREA } from '../../screens/timeline';
+import {
+ NOTES_AUTHOR,
+ NOTES_CODE_BLOCK,
+ NOTES_LINK,
+ NOTES_TEXT,
+ NOTES_TEXT_AREA,
+} from '../../screens/timeline';
import { createTimeline } from '../../tasks/api_calls/timelines';
import { cleanKibana } from '../../tasks/common';
@@ -16,6 +22,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import {
addNotesToTimeline,
closeTimeline,
+ goToNotesTab,
openTimelineById,
refreshTimelinesUntilTimeLinePresent,
} from '../../tasks/timeline';
@@ -23,8 +30,11 @@ import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines';
import { TIMELINES_URL } from '../../urls/navigation';
+const text = 'elastic';
+const link = 'https://www.elastic.co/';
+
describe('Timeline notes tab', () => {
- before(() => {
+ beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(TIMELINES_URL);
waitForTimelinesPanelToBeLoaded();
@@ -37,19 +47,62 @@ describe('Timeline notes tab', () => {
// request responses and indeterminism since on clicks to activates URL's.
.then(() => cy.wait(1000))
.then(() => openTimelineById(timelineId))
- .then(() => addNotesToTimeline(timelineNonValidQuery.notes))
+ .then(() => goToNotesTab())
);
});
after(() => {
closeTimeline();
});
+ it('should render mockdown', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(timelineNonValidQuery.notes);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(NOTES_TEXT_AREA).should('exist');
+ });
it('should contain notes', () => {
- cy.get(NOTES_TEXT).should('have.text', timelineNonValidQuery.notes);
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(timelineNonValidQuery.notes);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(NOTES_TEXT).first().should('have.text', timelineNonValidQuery.notes);
});
- it('should render mockdown', () => {
- cy.get(NOTES_TEXT_AREA).should('exist');
+ it('should be able to render font in bold', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(`**bold**`);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(`${NOTES_TEXT} strong`).last().should('have.text', `bold`);
+ });
+
+ it('should be able to render font in italics', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(`_italics_`);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(`${NOTES_TEXT} em`).last().should('have.text', `italics`);
+ });
+
+ it('should be able to render code blocks', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(`\`code\``);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(NOTES_CODE_BLOCK).should('exist');
+ });
+
+ it('should render the right author', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(timelineNonValidQuery.notes);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(NOTES_AUTHOR).first().should('have.text', text);
+ });
+
+ it('should be able to render a link', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ cy.intercept(link).as(`link`);
+ addNotesToTimeline(`[${text}](${link})`);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(NOTES_LINK).last().should('have.text', `${text}(opens in a new tab or window)`);
+ cy.get(NOTES_LINK).last().click();
+ cy.wait('@link').its('response.statusCode').should('eq', 200);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts
new file mode 100644
index 0000000000000..b569ea7cc082f
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ TIMELINE_EVENT,
+ TIMELINE_EVENTS_COUNT_NEXT_PAGE,
+ TIMELINE_EVENTS_COUNT_PER_PAGE,
+ TIMELINE_EVENTS_COUNT_PER_PAGE_BTN,
+ TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION,
+ TIMELINE_EVENTS_COUNT_PREV_PAGE,
+ TIMELINE_FLYOUT,
+} from '../../screens/timeline';
+import { cleanKibana } from '../../tasks/common';
+
+import { loginAndWaitForPage } from '../../tasks/login';
+import { openTimelineUsingToggle } from '../../tasks/security_main';
+import { populateTimeline } from '../../tasks/timeline';
+
+import { HOSTS_URL } from '../../urls/navigation';
+
+const defaultPageSize = 25;
+describe('Pagination', () => {
+ beforeEach(() => {
+ cleanKibana();
+ loginAndWaitForPage(HOSTS_URL);
+ openTimelineUsingToggle();
+ populateTimeline();
+ });
+
+ it(`should have ${defaultPageSize} events in the page by default`, () => {
+ cy.get(TIMELINE_EVENT).should('have.length', defaultPageSize);
+ });
+
+ it(`should select ${defaultPageSize} items per page by default`, () => {
+ cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', defaultPageSize);
+ });
+
+ it('should be able to change items count per page with the dropdown', () => {
+ const itemsPerPage = 100;
+ cy.intercept('POST', '/internal/bsearch').as('refetch');
+
+ cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_BTN).first().click();
+ cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION(itemsPerPage)).click();
+ cy.wait('@refetch').its('response.statusCode').should('eq', 200);
+ cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', itemsPerPage);
+ });
+
+ it('should be able to go to next / previous page', () => {
+ cy.intercept('POST', '/internal/bsearch').as('refetch');
+ cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_EVENTS_COUNT_NEXT_PAGE}`).first().click();
+ cy.wait('@refetch').its('response.statusCode').should('eq', 200);
+
+ cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_EVENTS_COUNT_PREV_PAGE}`).first().click();
+ cy.wait('@refetch').its('response.statusCode').should('eq', 200);
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts
index 672e930bc5072..f37a66ac048fb 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts
@@ -7,7 +7,13 @@
import { timeline } from '../../objects/timeline';
-import { UNLOCKED_ICON, PIN_EVENT, TIMELINE_FILTER, TIMELINE_QUERY } from '../../screens/timeline';
+import {
+ UNLOCKED_ICON,
+ PIN_EVENT,
+ TIMELINE_FILTER,
+ TIMELINE_QUERY,
+ NOTE_CARD_CONTENT,
+} from '../../screens/timeline';
import { addNoteToTimeline } from '../../tasks/api_calls/notes';
import { createTimeline } from '../../tasks/api_calls/timelines';
@@ -18,6 +24,7 @@ import {
addFilter,
closeTimeline,
openTimelineById,
+ persistNoteToFirstEvent,
pinFirstEvent,
refreshTimelinesUntilTimeLinePresent,
} from '../../tasks/timeline';
@@ -45,6 +52,7 @@ describe('Timeline query tab', () => {
)
.then(() => openTimelineById(timelineId))
.then(() => pinFirstEvent())
+ .then(() => persistNoteToFirstEvent('event note'))
.then(() => addFilter(timeline.filter));
});
});
@@ -58,6 +66,10 @@ describe('Timeline query tab', () => {
cy.get(TIMELINE_QUERY).should('have.text', `${timeline.query}`);
});
+ it('should be able to add event note', () => {
+ cy.get(NOTE_CARD_CONTENT).should('contain', 'event note');
+ });
+
it('should display timeline filter', () => {
cy.get(TIMELINE_FILTER(timeline.filter)).should('exist');
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts
new file mode 100644
index 0000000000000..ed9a7db4702d0
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright 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 {
+ TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN,
+ TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON,
+ TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX,
+ TIMELINE_ROW_RENDERERS_SEARCHBOX,
+ TIMELINE_SHOW_ROW_RENDERERS_GEAR,
+} from '../../screens/timeline';
+import { cleanKibana } from '../../tasks/common';
+
+import { loginAndWaitForPage } from '../../tasks/login';
+import { openTimelineUsingToggle } from '../../tasks/security_main';
+import { populateTimeline } from '../../tasks/timeline';
+
+import { HOSTS_URL } from '../../urls/navigation';
+
+const RowRenderersId = [
+ 'alerts',
+ 'auditd',
+ 'auditd_file',
+ 'library',
+ 'netflow',
+ 'plain',
+ 'registry',
+ 'suricata',
+ 'system',
+ 'system_dns',
+ 'system_endgame_process',
+ 'system_file',
+ 'system_fim',
+ 'system_security_event',
+ 'system_socket',
+ 'threat_match',
+ 'zeek',
+];
+
+describe('Row renderers', () => {
+ beforeEach(() => {
+ cleanKibana();
+ loginAndWaitForPage(HOSTS_URL);
+ openTimelineUsingToggle();
+ populateTimeline();
+ cy.get(TIMELINE_SHOW_ROW_RENDERERS_GEAR).first().click({ force: true });
+ });
+
+ afterEach(() => {
+ cy.get(TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON).click({ force: true });
+ });
+
+ it('Row renderers should be enabled by default', () => {
+ cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('exist');
+ cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('be.checked');
+ });
+
+ it('Selected renderer can be disabled and enabled', () => {
+ cy.get(TIMELINE_ROW_RENDERERS_SEARCHBOX).type('flow');
+
+ cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().uncheck();
+ cy.intercept('PATCH', '/api/timeline').as('updateTimeline');
+
+ cy.wait('@updateTimeline').then((interception) => {
+ expect(interception.request.body.timeline.excludedRowRendererIds).to.contain('netflow');
+ });
+
+ cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().check();
+
+ cy.wait('@updateTimeline').then((interception) => {
+ expect(interception.request.body.timeline.excludedRowRendererIds).not.to.contain('netflow');
+ });
+ });
+
+ it('Selected renderer can be disabled with one click', () => {
+ cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN).click({ force: true });
+
+ cy.intercept('PATCH', '/api/timeline').as('updateTimeline');
+ cy.wait('@updateTimeline').its('response.statusCode').should('eq', 200);
+
+ cy.wait('@updateTimeline').then((interception) => {
+ expect(interception.request.body.timeline.excludedRowRendererIds).to.eql(RowRenderersId);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts
index 48b00f8afd4eb..9d019cf23ebb1 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts
@@ -5,14 +5,21 @@
* 2.0.
*/
-import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline';
+import {
+ ADD_FILTER,
+ SERVER_SIDE_EVENT_COUNT,
+ TIMELINE_KQLMODE_FILTER,
+ TIMELINE_KQLMODE_SEARCH,
+ TIMELINE_SEARCH_OR_FILTER,
+} from '../../screens/timeline';
import { cleanKibana } from '../../tasks/common';
-import { loginAndWaitForPage } from '../../tasks/login';
+import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { openTimelineUsingToggle } from '../../tasks/security_main';
import { executeTimelineKQL } from '../../tasks/timeline';
+import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines';
-import { HOSTS_URL } from '../../urls/navigation';
+import { HOSTS_URL, TIMELINES_URL } from '../../urls/navigation';
describe('timeline search or filter KQL bar', () => {
beforeEach(() => {
@@ -28,3 +35,37 @@ describe('timeline search or filter KQL bar', () => {
cy.get(SERVER_SIDE_EVENT_COUNT).should(($count) => expect(+$count.text()).to.be.gt(0));
});
});
+
+describe('Update kqlMode for timeline', () => {
+ before(() => {
+ cleanKibana();
+ loginAndWaitForPageWithoutDateRange(TIMELINES_URL);
+ waitForTimelinesPanelToBeLoaded();
+ openTimelineUsingToggle();
+ });
+
+ beforeEach(() => {
+ cy.intercept('PATCH', '/api/timeline').as('update');
+ cy.get(TIMELINE_SEARCH_OR_FILTER)
+ .pipe(($el) => $el.trigger('click'))
+ .should('exist');
+ });
+
+ it('should be able to update timeline kqlMode with filter', () => {
+ cy.get(TIMELINE_KQLMODE_FILTER).click();
+ cy.wait('@update').then(({ response }) => {
+ cy.wrap(response!.statusCode).should('eql', 200);
+ cy.wrap(response!.body.data.persistTimeline.timeline.kqlMode).should('eql', 'filter');
+ cy.get(ADD_FILTER).should('exist');
+ });
+ });
+
+ it('should be able to update timeline kqlMode with search', () => {
+ cy.get(TIMELINE_KQLMODE_SEARCH).click();
+ cy.wait('@update').then(({ response }) => {
+ cy.wrap(response!.statusCode).should('eql', 200);
+ cy.wrap(response!.body.data.persistTimeline.timeline.kqlMode).should('eql', 'search');
+ cy.get(ADD_FILTER).should('not.exist');
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts
index 1c519b21149a8..ce6c5662ecb9e 100644
--- a/x-pack/plugins/security_solution/cypress/screens/overview.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts
@@ -145,3 +145,5 @@ export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]';
export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]';
export const OVERVIEW_EMPTY_PAGE = '[data-test-subj="empty-page"]';
+
+export const OVERVIEW_REVENT_TIMELINES = '[data-test-subj="overview-recent-timelines"]';
diff --git a/x-pack/plugins/security_solution/cypress/screens/security_header.ts b/x-pack/plugins/security_solution/cypress/screens/security_header.ts
index cb8502ef96029..a3d5b714cdb3f 100644
--- a/x-pack/plugins/security_solution/cypress/screens/security_header.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/security_header.ts
@@ -24,3 +24,5 @@ export const OVERVIEW = '[data-test-subj="navigation-overview"]';
export const REFRESH_BUTTON = '[data-test-subj="querySubmitButton"]';
export const TIMELINES = '[data-test-subj="navigation-timelines"]';
+
+export const LOADING_INDICATOR = '[data-test-subj="globalLoadingIndicator"]';
diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts
index 88e207fcea339..63c4c1364fcd0 100644
--- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts
@@ -58,6 +58,10 @@ export const UNLOCKED_ICON = '[data-test-subj="timeline-date-picker-unlock-butto
export const NOTES = '[data-test-subj="note-card-body"]';
+export const NOTE_CARD_CONTENT = '[data-test-subj="notes"]';
+
+export const EVENT_NOTE = '[data-test-subj="timeline-notes-button-small"]';
+
export const NOTE_BY_NOTE_ID = (noteId: string) =>
`[data-test-subj="note-preview-${noteId}"] .euiMarkdownFormat`;
@@ -69,6 +73,12 @@ export const NOTES_TAB_BUTTON = '[data-test-subj="timelineTabs-notes"]';
export const NOTES_TEXT = '.euiMarkdownFormat';
+export const NOTES_CODE_BLOCK = '.euiCodeBlock__code';
+
+export const NOTES_AUTHOR = '.euiCommentEvent__headerUsername';
+
+export const NOTES_LINK = '[data-test-subj="markdown-link"]';
+
export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]';
export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]';
@@ -110,6 +120,8 @@ export const PINNED_TAB_EVENTS_BODY = '[data-test-subj="pinned-tab-flyout-body"]
export const PINNED_TAB_EVENTS_FOOTER = '[data-test-subj="pinned-tab-flyout-footer"]';
+export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]';
+
export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]';
export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]';
@@ -118,8 +130,21 @@ export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiPro
export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]';
+export const TIMELINE_COLLAPSED_ITEMS_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]';
+
+export const TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN =
+ '[data-test-subj="create-template-from-timeline"]';
+
+export const TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN = '[data-test-subj="create-from-template"]';
+
+export const TIMELINE_CORRELATION_INPUT = '[data-test-subj="eqlQueryBarTextInput"]';
+
+export const TIMELINE_CORRELATION_TAB = '[data-test-subj="timelineTabs-eql"]';
+
export const IS_DRAGGING_DATA_PROVIDERS = '.is-dragging';
+export const TIMELINE_BOTTOM_BAR_CONTAINER = '[data-test-subj="timeline-bottom-bar-container"]';
+
export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]';
export const TIMELINE_DATA_PROVIDERS_ACTION_MENU = '[data-test-subj="providerActions"]';
@@ -143,6 +168,21 @@ export const TIMELINE_DESCRIPTION_INPUT = '[data-test-subj="save-timeline-descri
export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContainer"]';
+export const TIMELINE_EVENT = '[data-test-subj="event"]';
+
+export const TIMELINE_EVENTS_COUNT_PER_PAGE = '[data-test-subj="local-events-count"]';
+
+export const TIMELINE_EVENTS_COUNT_PER_PAGE_BTN = '[data-test-subj="local-events-count-button"]';
+
+export const TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION = (itemsPerPage: number) =>
+ `[data-test-subj="items-per-page-option-${itemsPerPage}"]`;
+
+export const TIMELINE_EVENTS_COUNT_NEXT_PAGE =
+ '[data-test-subj="timeline"] [data-test-subj="pagination-button-next"]';
+
+export const TIMELINE_EVENTS_COUNT_PREV_PAGE =
+ '[data-test-subj="timeline"] [data-test-subj="pagination-button-previous"]';
+
export const TIMELINE_FIELDS_BUTTON =
'[data-test-subj="timeline"] [data-test-subj="show-field-browser"]';
@@ -164,6 +204,8 @@ export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="query-tab-flyout-header"
export const TIMELINE_FLYOUT_BODY = '[data-test-subj="query-tab-flyout-body"]';
+export const TIMELINE_HEADER = '[data-test-subj="timeline-hide-show-container"]';
+
export const TIMELINE_INSPECT_BUTTON = `${TIMELINE_FLYOUT} [data-test-subj="inspect-icon-button"]`;
export const TIMELINE_PANEL = `[data-test-subj="timeline-flyout-header-panel"]`;
@@ -172,6 +214,14 @@ export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]';
export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-plus-in-circle"]';
+export const TIMELINE_SEARCH_OR_FILTER = '[data-test-subj="timeline-select-search-or-filter"]';
+
+export const TIMELINE_SEARCH_OR_FILTER_CONTENT = '.searchOrFilterPopover';
+
+export const TIMELINE_KQLMODE_SEARCH = '[data-test-subj="kqlModePopoverSearch"]';
+
+export const TIMELINE_KQLMODE_FILTER = '[data-test-subj="kqlModePopoverFilter"]';
+
export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]';
export const TIMELINE_TITLE_INPUT = '[data-test-subj="save-timeline-title"]';
@@ -186,4 +236,33 @@ export const TIMELINE_EDIT_MODAL_OPEN_BUTTON = '[data-test-subj="save-timeline-b
export const TIMELINE_EDIT_MODAL_SAVE_BUTTON = '[data-test-subj="save-button"]';
-export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]';
+export const TIMELINE_EXIT_FULL_SCREEN_BUTTON = '[data-test-subj="exit-full-screen"]';
+
+export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="flyout-pane"]';
+
+export const TIMELINE_FULL_SCREEN_BUTTON = '[data-test-subj="full-screen-active"]';
+
+export const TIMELINE_ROW_RENDERERS_MODAL = '[data-test-subj="row-renderers-modal"]';
+
+export const TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN = `[data-test-subj="disable-all"]`;
+
+export const TIMELINE_ROW_RENDERERS_ENABLE_ALL_BTN = `button[data-test-subj="enable-alll"]`;
+
+export const TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON = `${TIMELINE_ROW_RENDERERS_MODAL} .euiModal__closeIcon`;
+
+export const TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX = `${TIMELINE_ROW_RENDERERS_MODAL} .euiCheckbox__input`;
+
+export const TIMELINE_ROW_RENDERERS_SEARCHBOX = `${TIMELINE_ROW_RENDERERS_MODAL} input[type="search"]`;
+
+export const TIMELINE_SHOW_ROW_RENDERERS_GEAR = '[data-test-subj="show-row-renderers-gear"]';
+
+export const TIMELINE_TABS = '[data-test-subj="timeline"] .euiTabs';
+
+export const TIMELINE_TAB_CONTENT_EQL = '[data-test-subj="timeline-tab-content-eql"]';
+
+export const TIMELINE_TAB_CONTENT_QUERY = '[data-test-subj="timeline-tab-content-query"]';
+
+export const TIMELINE_TAB_CONTENT_PINNED = '[data-test-subj="timeline-tab-content-pinned"]';
+
+export const TIMELINE_TAB_CONTENT_GRAPHS_NOTES =
+ '[data-test-subj="timeline-tab-content-graph-notes"]';
diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js
index 90eb9a38d7509..e74d06cd621fb 100644
--- a/x-pack/plugins/security_solution/cypress/support/commands.js
+++ b/x-pack/plugins/security_solution/cypress/support/commands.js
@@ -35,7 +35,7 @@ Cypress.Commands.add(
'stubSearchStrategyApi',
function (stubObject, factoryQueryType, searchStrategyName = 'securitySolutionSearchStrategy') {
cy.intercept('POST', '/internal/bsearch', (req) => {
- if (searchStrategyName === 'securitySolutionIndexFields') {
+ if (searchStrategyName === 'indexFields') {
req.reply(stubObject.rawResponse);
} else if (factoryQueryType === 'overviewHost') {
req.reply(stubObject.overviewHost);
diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts
index 18359574633e9..8274d19f77a25 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts
@@ -119,3 +119,26 @@ export const loadPrepackagedTimelineTemplates = () =>
url: 'api/timeline/_prepackaged',
headers: { 'kbn-xsrf': 'cypress-creds' },
});
+
+export const favoriteTimeline = ({
+ timelineId,
+ timelineType,
+ templateTimelineId,
+ templateTimelineVersion,
+}: {
+ timelineId: string;
+ timelineType: string;
+ templateTimelineId?: string;
+ templateTimelineVersion?: number;
+}) =>
+ cy.request({
+ method: 'PATCH',
+ url: 'api/timeline/_favorite',
+ body: {
+ timelineId,
+ timelineType,
+ templateTimelineId: templateTimelineId || null,
+ templateTimelineVersion: templateTimelineVersion || null,
+ },
+ headers: { 'kbn-xsrf': 'cypress-creds' },
+ });
diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts
index 189ef1e46e4bc..01651b7b943d0 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts
@@ -11,6 +11,7 @@ import {
TIMELINE_TOGGLE_BUTTON,
TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON,
} from '../screens/security_main';
+import { TIMELINE_EXIT_FULL_SCREEN_BUTTON, TIMELINE_FULL_SCREEN_BUTTON } from '../screens/timeline';
export const openTimelineUsingToggle = () => {
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).click();
@@ -30,3 +31,11 @@ export const openTimelineIfClosed = () =>
openTimelineUsingToggle();
}
});
+
+export const enterFullScreenMode = () => {
+ cy.get(TIMELINE_FULL_SCREEN_BUTTON).first().click({ force: true });
+};
+
+export const exitFullScreenMode = () => {
+ cy.get(TIMELINE_EXIT_FULL_SCREEN_BUTTON).first().click({ force: true });
+};
diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
index 587e4ec45b8c7..af7a7bb5d4c71 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
@@ -8,6 +8,7 @@
import { Timeline, TimelineFilter } from '../objects/timeline';
import { ALL_CASES_CREATE_NEW_CASE_TABLE_BTN } from '../screens/all_cases';
+import { LOADING_INDICATOR } from '../screens/security_header';
import {
ADD_FILTER,
@@ -56,6 +57,13 @@ import {
TIMELINE_DATA_PROVIDER_OPERATOR,
TIMELINE_DATA_PROVIDER_VALUE,
SAVE_DATA_PROVIDER_BTN,
+ EVENT_NOTE,
+ TIMELINE_CORRELATION_INPUT,
+ TIMELINE_CORRELATION_TAB,
+ TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN,
+ TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN,
+ TIMELINE_COLLAPSED_ITEMS_BTN,
+ TIMELINE_TAB_CONTENT_EQL,
} from '../screens/timeline';
import { REFRESH_BUTTON, TIMELINE } from '../screens/timelines';
@@ -99,6 +107,16 @@ export const goToNotesTab = (): Cypress.Chainable> => {
return cy.root().find(NOTES_TAB_BUTTON);
};
+export const goToCorrelationTab = () => {
+ cy.root()
+ .pipe(($el) => {
+ $el.find(TIMELINE_CORRELATION_TAB).trigger('click');
+ return $el.find(`${TIMELINE_TAB_CONTENT_EQL} ${TIMELINE_CORRELATION_INPUT}`);
+ })
+ .should('be.visible');
+ return cy.root().find(TIMELINE_CORRELATION_TAB);
+};
+
export const getNotePreviewByNoteId = (noteId: string) => {
return cy.get(`[data-test-subj="note-preview-${noteId}"]`);
};
@@ -127,6 +145,12 @@ export const addNotesToTimeline = (notes: string) => {
goToNotesTab();
};
+export const addEqlToTimeline = (eql: string) => {
+ goToCorrelationTab().then(() => {
+ cy.get(TIMELINE_CORRELATION_INPUT).type(eql);
+ });
+};
+
export const addFilter = (filter: TimelineFilter): Cypress.Chainable> => {
cy.get(ADD_FILTER).click();
cy.get(TIMELINE_FILTER_FIELD).type(`${filter.field}{downarrow}{enter}`);
@@ -140,7 +164,8 @@ export const addFilter = (filter: TimelineFilter): Cypress.Chainable> => {
cy.get(TIMELINE_ADD_FIELD_BUTTON).click();
- cy.wait(300);
+ cy.get(TIMELINE_DATA_PROVIDER_VALUE).should('have.focus'); // make sure the focus is ready before start typing
+
cy.get(TIMELINE_DATA_PROVIDER_FIELD).type(`${filter.field}{downarrow}{enter}`);
cy.get(TIMELINE_DATA_PROVIDER_OPERATOR).type(filter.operator);
cy.get(COMBO_BOX).contains(filter.operator).click();
@@ -209,8 +234,10 @@ export const expandFirstTimelineEventDetails = () => {
cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true });
};
-export const markAsFavorite = (): Cypress.Chainable> => {
- return cy.get(STAR_ICON).click();
+export const markAsFavorite = () => {
+ const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click();
+ cy.get(STAR_ICON).should('be.visible').pipe(click);
+ cy.get(LOADING_INDICATOR).should('not.exist');
};
export const openTimelineFieldsBrowser = () => {
@@ -249,6 +276,15 @@ export const pinFirstEvent = (): Cypress.Chainable> => {
return cy.get(PIN_EVENT).first().click({ force: true });
};
+export const persistNoteToFirstEvent = (notes: string) => {
+ cy.get(EVENT_NOTE).first().click({ force: true });
+ cy.get(NOTES_TEXT_AREA).type(notes);
+ cy.root().pipe(($el) => {
+ $el.find(ADD_NOTE_BUTTON).trigger('click');
+ return $el.find(NOTES_TAB_BUTTON).find('.euiBadge');
+ });
+};
+
export const populateTimeline = () => {
executeTimelineKQL(hostExistsQuery);
cy.get(SERVER_SIDE_EVENT_COUNT).should('not.have.text', '0');
@@ -325,3 +361,15 @@ export const refreshTimelinesUntilTimeLinePresent = (
})
.should('be.visible');
};
+
+export const clickingOnCreateTimelineFormTemplateBtn = () => {
+ cy.get(TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN).click({ force: true });
+};
+
+export const clickingOnCreateTemplateFromTimelineBtn = () => {
+ cy.get(TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN).click({ force: true });
+};
+
+export const expandEventAction = () => {
+ cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).first().click();
+};
diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json
index 02dbc56bd3397..e26f0d9b65bfa 100644
--- a/x-pack/plugins/security_solution/kibana.json
+++ b/x-pack/plugins/security_solution/kibana.json
@@ -17,6 +17,7 @@
"inspector",
"licensing",
"maps",
+ "timelines",
"triggersActionsUi",
"uiActions"
],
diff --git a/x-pack/plugins/security_solution/public/app/404.tsx b/x-pack/plugins/security_solution/public/app/404.tsx
index c21f7a4d4d578..2634ffd47bff1 100644
--- a/x-pack/plugins/security_solution/public/app/404.tsx
+++ b/x-pack/plugins/security_solution/public/app/404.tsx
@@ -8,15 +8,15 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { WrapperPage } from '../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper';
export const NotFoundPage = React.memo(() => (
-
+
-
+
));
NotFoundPage.displayName = 'NotFoundPage';
diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx
index cfb25c4436db3..c223570c77201 100644
--- a/x-pack/plugins/security_solution/public/app/app.tsx
+++ b/x-pack/plugins/security_solution/public/app/app.tsx
@@ -11,7 +11,7 @@ import { Store, Action } from 'redux';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { EuiErrorBoundary } from '@elastic/eui';
-import { AppLeaveHandler } from '../../../../../src/core/public';
+import { AppLeaveHandler, AppMountParameters } from '../../../../../src/core/public';
import { ManageUserInfo } from '../detections/components/user_info';
import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants';
@@ -21,7 +21,6 @@ import { GlobalToaster, ManageGlobalToaster } from '../common/components/toaster
import { KibanaContextProvider, useKibana, useUiSetting$ } from '../common/lib/kibana';
import { State } from '../common/store';
-import { ManageGlobalTimeline } from '../timelines/components/manage_timeline';
import { StartServices } from '../types';
import { PageRouter } from './routes';
import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
@@ -31,10 +30,17 @@ interface StartAppComponent {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
store: Store;
}
-const StartAppComponent: FC = ({ children, history, onAppLeave, store }) => {
+const StartAppComponent: FC = ({
+ children,
+ history,
+ setHeaderActionMenu,
+ onAppLeave,
+ store,
+}) => {
const { i18n } = useKibana().services;
const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE);
@@ -42,23 +48,25 @@ const StartAppComponent: FC = ({ children, history, onAppLeav
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
@@ -72,6 +80,7 @@ interface SecurityAppComponentProps {
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
services: StartServices;
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
store: Store;
}
@@ -80,6 +89,7 @@ const SecurityAppComponent: React.FC = ({
history,
onAppLeave,
services,
+ setHeaderActionMenu,
store,
}) => (
= ({
...services,
}}
>
-
+
{children}
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
new file mode 100644
index 0000000000000..98ff11423ce01
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import {
+ EuiHeaderSection,
+ EuiHeaderLinks,
+ EuiHeaderLink,
+ EuiHeaderSectionItem,
+} from '@elastic/eui';
+import React, { useEffect, useMemo } from 'react';
+import { createPortalNode, OutPortal, InPortal } from 'react-reverse-portal';
+import { i18n } from '@kbn/i18n';
+
+import { AppMountParameters } from '../../../../../../../src/core/public';
+import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
+import { MlPopover } from '../../../common/components/ml_popover/ml_popover';
+import { useKibana } from '../../../common/lib/kibana';
+import { ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants';
+
+const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.buttonAddData', {
+ defaultMessage: 'Add data',
+});
+
+/**
+ * This component uses the reverse portal to add the Add Data and ML job settings buttons on the
+ * right hand side of the Kibana global header
+ */
+export const GlobalHeader = React.memo(
+ ({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => {
+ const portalNode = useMemo(() => createPortalNode(), []);
+ const { http } = useKibana().services;
+
+ useEffect(() => {
+ let unmount = () => {};
+
+ setHeaderActionMenu((element) => {
+ const mount = toMountPoint( );
+ unmount = mount(element);
+ return unmount;
+ });
+
+ return () => {
+ portalNode.unmount();
+ unmount();
+ };
+ }, [portalNode, setHeaderActionMenu]);
+
+ return (
+
+
+ {window.location.pathname.includes(APP_DETECTIONS_PATH) && (
+
+
+
+ )}
+
+
+
+ {BUTTON_ADD_DATA}
+
+
+
+
+
+ );
+ }
+);
+GlobalHeader.displayName = 'GlobalHeader';
diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
index 7ebcc96753836..8358e2f9377b8 100644
--- a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import * as i18n from './translations';
+import * as i18n from '../translations';
import { SecurityPageName } from '../types';
import { SiemNavTab } from '../../common/components/navigation/types';
import {
diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx
index 1b0ddcfb9ae7d..9a57ab3fc3a73 100644
--- a/x-pack/plugins/security_solution/public/app/home/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/index.tsx
@@ -5,57 +5,35 @@
* 2.0.
*/
-import React, { useEffect, useRef, useState } from 'react';
-import styled from 'styled-components';
+import React, { useRef } from 'react';
-import { TimelineId } from '../../../common/types/timeline';
import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper';
-import { Flyout } from '../../timelines/components/flyout';
+import { AppLeaveHandler, AppMountParameters } from '../../../../../../src/core/public';
import { SecuritySolutionAppWrapper } from '../../common/components/page';
-import { HeaderGlobal } from '../../common/components/header_global';
import { HelpMenu } from '../../common/components/help_menu';
-import { AutoSaveWarningMsg } from '../../timelines/components/timeline/auto_save_warning';
import { UseUrlState } from '../../common/components/url_state';
-import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline';
import { navTabs } from './home_navigations';
import { useInitSourcerer, useSourcererScope } from '../../common/containers/sourcerer';
import { useKibana } from '../../common/lib/kibana';
import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants';
import { SourcererScopeName } from '../../common/store/sourcerer/model';
import { useUpgradeEndpointPackage } from '../../common/hooks/endpoint/upgrade';
-import { useThrottledResizeObserver } from '../../common/components/utils';
-import { AppLeaveHandler } from '../../../../../../src/core/public';
-
-const Main = styled.main.attrs<{ paddingTop: number }>(({ paddingTop }) => ({
- style: {
- paddingTop: `${paddingTop}px`,
- },
-}))<{ paddingTop: number }>`
- overflow: auto;
- display: flex;
- flex-direction: column;
- flex: 1 1 auto;
-`;
-
-Main.displayName = 'Main';
+import { GlobalHeader } from './global_header';
+import { SecuritySolutionTemplateWrapper } from './template_wrapper';
interface HomePageProps {
children: React.ReactNode;
onAppLeave: (handler: AppLeaveHandler) => void;
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
}
-const HomePageComponent: React.FC = ({ children, onAppLeave }) => {
- const { application, overlays } = useKibana().services;
+const HomePageComponent: React.FC = ({
+ children,
+ onAppLeave,
+ setHeaderActionMenu,
+}) => {
+ const { application } = useKibana().services;
const subPluginId = useRef('');
- const { ref, height = 0 } = useThrottledResizeObserver(300);
- const banners$ = overlays.banners.get$();
- const [headerFixed, setHeaderFixed] = useState(true);
- const mainPaddingTop = headerFixed ? height : 0;
-
- useEffect(() => {
- const subscription = banners$.subscribe((banners) => setHeaderFixed(!banners.length));
- return () => subscription.unsubscribe();
- }, [banners$]); // Only un/re-subscribe if the Observable changes
application.currentAppId$.subscribe((appId) => {
subPluginId.current = appId ?? '';
@@ -66,13 +44,13 @@ const HomePageComponent: React.FC = ({ children, onAppLeave }) =>
? SourcererScopeName.detections
: SourcererScopeName.default
);
- const [showTimeline] = useShowTimeline();
- const { browserFields, indexPattern, indicesExist } = useSourcererScope(
+ const { browserFields, indexPattern } = useSourcererScope(
subPluginId.current === DETECTIONS_SUB_PLUGIN_ID
? SourcererScopeName.detections
: SourcererScopeName.default
);
+
// side effect: this will attempt to upgrade the endpoint package if it is not up to date
// this will run when a user navigates to the Security Solution app and when they navigate between
// tabs in the app. This is useful for keeping the endpoint package as up to date as possible until
@@ -81,23 +59,14 @@ const HomePageComponent: React.FC = ({ children, onAppLeave }) =>
useUpgradeEndpointPackage();
return (
-
-
-
-
-
-
- {indicesExist && showTimeline && (
- <>
-
-
- >
- )}
-
+
+
+
+
+
{children}
-
-
-
+
+
);
diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx
new file mode 100644
index 0000000000000..08ebbeaee55d4
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* eslint-disable react/display-name */
+
+import React, { useRef } from 'react';
+import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public';
+import { AppLeaveHandler } from '../../../../../../../../src/core/public';
+import { useKibana } from '../../../../common/lib/kibana';
+import { useShowTimeline } from '../../../../common/utils/timeline/use_show_timeline';
+import { useSourcererScope } from '../../../../common/containers/sourcerer';
+import { DETECTIONS_SUB_PLUGIN_ID } from '../../../../../common/constants';
+import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
+import { TimelineId } from '../../../../../common/types/timeline';
+import { AutoSaveWarningMsg } from '../../../../timelines/components/timeline/auto_save_warning';
+import { Flyout } from '../../../../timelines/components/flyout';
+
+export const BOTTOM_BAR_CLASSNAME = 'timeline-bottom-bar';
+
+export const SecuritySolutionBottomBar = React.memo(
+ ({ onAppLeave }: { onAppLeave: (handler: AppLeaveHandler) => void }) => {
+ const subPluginId = useRef('');
+ const { application } = useKibana().services;
+ application.currentAppId$.subscribe((appId) => {
+ subPluginId.current = appId ?? '';
+ });
+
+ const [showTimeline] = useShowTimeline();
+
+ const { indicesExist } = useSourcererScope(
+ subPluginId.current === DETECTIONS_SUB_PLUGIN_ID
+ ? SourcererScopeName.detections
+ : SourcererScopeName.default
+ );
+
+ return indicesExist && showTimeline ? (
+ <>
+
+
+ >
+ ) : null;
+ }
+);
+
+export const SecuritySolutionBottomBarProps: KibanaPageTemplateProps['bottomBarProps'] = {
+ className: BOTTOM_BAR_CLASSNAME,
+ 'data-test-subj': 'timeline-bottom-bar-container',
+ position: 'fixed',
+ usePortal: false,
+};
diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx
new file mode 100644
index 0000000000000..3e3c91133eab6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import styled from 'styled-components';
+import { OutPortal } from 'react-reverse-portal';
+import { useGlobalHeaderPortal } from '../../../../common/hooks/use_global_header_portal';
+
+const StyledStickyWrapper = styled.div`
+ position: sticky;
+ z-index: ${(props) => props.theme.eui.euiZLevel2};
+ // TOP location is declared in src/public/rendering/_base.scss to keep in line with Kibana Chrome
+`;
+
+export const GlobalKQLHeader = React.memo(() => {
+ const { globalKQLHeaderPortalNode } = useGlobalHeaderPortal();
+
+ return (
+
+
+
+ );
+});
+
+GlobalKQLHeader.displayName = 'GlobalKQLHeader';
diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx
new file mode 100644
index 0000000000000..02fd07151f111
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx
@@ -0,0 +1,96 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo } from 'react';
+import styled from 'styled-components';
+import { EuiPanel } from '@elastic/eui';
+import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
+import { AppLeaveHandler } from '../../../../../../../src/core/public';
+import { KibanaPageTemplate } from '../../../../../../../src/plugins/kibana_react/public';
+import { useSecuritySolutionNavigation } from '../../../common/components/navigation/use_security_solution_navigation';
+import { TimelineId } from '../../../../common/types/timeline';
+import { getTimelineShowStatusByIdSelector } from '../../../timelines/components/flyout/selectors';
+import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
+import { GlobalKQLHeader } from './global_kql_header';
+import {
+ BOTTOM_BAR_CLASSNAME,
+ SecuritySolutionBottomBar,
+ SecuritySolutionBottomBarProps,
+} from './bottom_bar';
+import { useShowTimeline } from '../../../common/utils/timeline/use_show_timeline';
+import { gutterTimeline } from '../../../common/lib/helpers';
+
+/* eslint-disable react/display-name */
+
+/**
+ * Need to apply the styles via a className to effect the containing bottom bar
+ * rather than applying them to the timeline bar directly
+ */
+const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{
+ $isShowingTimelineOverlay?: boolean;
+ $isTimelineBottomBarVisible?: boolean;
+}>`
+ .${BOTTOM_BAR_CLASSNAME} {
+ animation: 'none !important'; // disable the default bottom bar slide animation
+ background: ${({ theme }) =>
+ theme.eui.euiColorEmptyShade}; // Override bottom bar black background
+ color: inherit; // Necessary to override the bottom bar 'white text'
+ transform: ${(
+ { $isShowingTimelineOverlay } // Since the bottom bar wraps the whole overlay now, need to override any transforms when it is open
+ ) => ($isShowingTimelineOverlay ? 'none' : 'translateY(calc(100% - 50px))')};
+ z-index: ${({ theme }) => theme.eui.euiZLevel8};
+
+ .${IS_DRAGGING_CLASS_NAME} & {
+ // When a drag is in process the bottom flyout should slide up to allow a drop
+ transform: none;
+ }
+ }
+
+ // If the bottom bar is visible add padding to the navigation
+ ${({ $isTimelineBottomBarVisible }) =>
+ $isTimelineBottomBarVisible &&
+ `
+ @media (min-width: 768px) {
+ .kbnPageTemplateSolutionNav {
+ padding-bottom: ${gutterTimeline};
+ }
+ }
+ `}
+`;
+
+interface SecuritySolutionPageWrapperProps {
+ onAppLeave: (handler: AppLeaveHandler) => void;
+}
+
+export const SecuritySolutionTemplateWrapper: React.FC = React.memo(
+ ({ children, onAppLeave }) => {
+ const solutionNav = useSecuritySolutionNavigation();
+ const [isTimelineBottomBarVisible] = useShowTimeline();
+ const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
+ const { show: isShowingTimelineOverlay } = useDeepEqualSelector((state) =>
+ getTimelineShowStatus(state, TimelineId.active)
+ );
+
+ return (
+ }
+ paddingSize="none"
+ solutionNav={solutionNav}
+ restrictWidth={false}
+ template="default"
+ >
+
+
+ {children}
+
+
+ );
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx
index 1e304c2686960..194f119e35478 100644
--- a/x-pack/plugins/security_solution/public/app/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/index.tsx
@@ -15,12 +15,19 @@ export const renderApp = ({
element,
history,
onAppLeave,
+ setHeaderActionMenu,
services,
store,
SubPluginRoutes,
}: RenderAppProps): (() => void) => {
render(
-
+
,
element
diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx
index 6454653af5214..a9a94a6998286 100644
--- a/x-pack/plugins/security_solution/public/app/routes.tsx
+++ b/x-pack/plugins/security_solution/public/app/routes.tsx
@@ -10,7 +10,7 @@ import React, { FC, memo, useEffect } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { useDispatch } from 'react-redux';
-import { AppLeaveHandler } from '../../../../../src/core/public';
+import { AppLeaveHandler, AppMountParameters } from '../../../../../src/core/public';
import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes';
import { RouteCapture } from '../common/components/endpoint/route_capture';
import { AppAction } from '../common/store/actions';
@@ -21,9 +21,15 @@ interface RouterProps {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
}
-const PageRouterComponent: FC = ({ children, history, onAppLeave }) => {
+const PageRouterComponent: FC = ({
+ children,
+ history,
+ onAppLeave,
+ setHeaderActionMenu,
+}) => {
const dispatch = useDispatch<(action: AppAction) => void>();
useEffect(() => {
return () => {
@@ -42,7 +48,9 @@ const PageRouterComponent: FC = ({ children, history, onAppLeave })