diff --git a/docs/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc
index 267ab3891d7000..5bd3a7587dde9e 100644
--- a/docs/api/saved-objects/bulk_create.asciidoc
+++ b/docs/api/saved-objects/bulk_create.asciidoc
@@ -45,6 +45,11 @@ experimental[] Create multiple {kib} saved objects.
(Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the
object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space
(default behavior).
+* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including
+the "All spaces" identifier (`'*'`).
+* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be
+used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
`version`::
(Optional, number) Specifies the version.
diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc
index d7a368034ef07f..e7e25c7d3bba6d 100644
--- a/docs/api/saved-objects/create.asciidoc
+++ b/docs/api/saved-objects/create.asciidoc
@@ -52,6 +52,11 @@ any data that you send to the API is properly formed.
(Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the
object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space
(default behavior).
+* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including
+the "All spaces" identifier (`'*'`).
+* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be
+used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+* For global object types (registered with `namespaceType: 'agnostic'): this option cannot be used.
[[saved-objects-api-create-request-codes]]
==== Response code
diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc
index 48d0d40d0abb06..e8b950a696f55d 100644
--- a/docs/developer/getting-started/monorepo-packages.asciidoc
+++ b/docs/developer/getting-started/monorepo-packages.asciidoc
@@ -104,6 +104,7 @@ yarn kbn watch-bazel
- @kbn/storybook
- @kbn/telemetry-utils
- @kbn/tinymath
+- @kbn/ui-framework
- @kbn/ui-shared-deps
- @kbn/utility-types
- @kbn/utils
diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
index d3d76079cdc2a1..ae433e3db14c68 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
@@ -116,6 +116,7 @@ readonly links: {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
index 34279cef198bfb..b0800c7dfc65ea 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
@@ -17,5 +17,5 @@ export interface DocLinksStart
| --- | --- | --- |
| [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string
| |
| [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string
| |
-| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
}
| |
+| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
}
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
index 3db8bbadfbd6bf..4d094ecde7a96a 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
@@ -6,7 +6,7 @@
Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).
-Note: this can only be used for multi-namespace object types.
+\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
index 6fc01212a2e41a..463c3fe81b7029 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
@@ -18,7 +18,7 @@ export interface SavedObjectsBulkCreateObject
| [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T
| |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
| [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string
| |
-| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. |
+| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'
): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'
). \* For isolated object types (registered with namespaceType: 'single'
or namespaceType: 'multiple-isolated'
): this option can only be used to specify a single space, and the "All spaces" identifier ('*'
) is not allowed. \* For global object types (registered with namespaceType: 'agnostic'
): this option cannot be used. |
| [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion
| Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [originId](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) | string
| Optional ID of the original saved object, if this object's id
was regenerated |
| [references](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[]
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
index 262b0997cb9050..43489b8d2e8a27 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
@@ -6,7 +6,7 @@
Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).
-Note: this can only be used for multi-namespace object types.
+\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
index 1805f389d4e7f3..7eaa9c51f5c82c 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
@@ -17,7 +17,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions
| --- | --- | --- |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
| [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string
| (not recommended) Specify an id for the document |
-| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. |
+| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'
): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'
). \* For isolated object types (registered with namespaceType: 'single'
or namespaceType: 'multiple-isolated'
): this option can only be used to specify a single space, and the "All spaces" identifier ('*'
) is not allowed. \* For global object types (registered with namespaceType: 'agnostic'
): this option cannot be used. |
| [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion
| Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [originId](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) | string
| Optional ID of the original saved object, if this object's id
was regenerated |
| [overwrite](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | boolean
| Overwrite existing documents (defaults to false) |
diff --git a/package.json b/package.json
index 873dffeed38f8a..36fa086657adff 100644
--- a/package.json
+++ b/package.json
@@ -154,7 +154,7 @@
"@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",
@@ -446,8 +446,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",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index 70a3d1eacc7c58..b1c3f580c6baf8 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -48,6 +48,7 @@ filegroup(
"//packages/kbn-storybook:build",
"//packages/kbn-telemetry-tools:build",
"//packages/kbn-tinymath:build",
+ "//packages/kbn-ui-framework:build",
"//packages/kbn-ui-shared-deps:build",
"//packages/kbn-utility-types:build",
"//packages/kbn-utils:build",
diff --git a/packages/kbn-ui-framework/BUILD.bazel b/packages/kbn-ui-framework/BUILD.bazel
new file mode 100644
index 00000000000000..f8cf5035bdc5f3
--- /dev/null
+++ b/packages/kbn-ui-framework/BUILD.bazel
@@ -0,0 +1,47 @@
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-ui-framework"
+PKG_REQUIRE_NAME = "@kbn/ui-framework"
+
+SOURCE_FILES = glob([
+ "dist/**/*",
+])
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "package.json",
+ "README.md",
+]
+
+DEPS = []
+
+js_library(
+ name = PKG_BASE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES + [
+ ":srcs",
+ ],
+ deps = DEPS,
+ package_name = PKG_REQUIRE_NAME,
+ visibility = ["//visibility:public"],
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [
+ ":%s" % PKG_BASE_NAME,
+ ]
+)
+
+filegroup(
+ name = "build",
+ srcs = [
+ ":npm_module",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 95091a761639b6..8c52d09f821595 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -137,6 +137,7 @@ export class DocLinksService {
addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`,
kibana: `${KIBANA_DOCS}index.html`,
upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`,
+ rollupJobs: `${KIBANA_DOCS}data-rollups.html`,
elasticsearch: {
docsBase: `${ELASTICSEARCH_DOCS}`,
asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`,
@@ -532,6 +533,7 @@ export interface DocLinksStart {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 6cc2b3f321fb7c..27569935bcc65f 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -595,6 +595,7 @@ export interface DocLinksStart {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 22c40a547f419a..4456784fdbc0b4 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -525,15 +525,22 @@ describe('SavedObjectsRepository', () => {
const ns2 = 'bar-namespace';
const ns3 = 'baz-namespace';
const objects = [
- { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2] },
- { ...obj2, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns3] },
+ { ...obj1, type: 'dashboard', initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] },
];
await bulkCreateSuccess(objects, { namespace, overwrite: true });
const body = [
- expect.any(Object),
+ { index: expect.objectContaining({ _id: `${ns2}:dashboard:${obj1.id}` }) },
+ expect.objectContaining({ namespace: ns2 }),
+ {
+ index: expect.objectContaining({
+ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj1.id}`,
+ }),
+ },
expect.objectContaining({ namespaces: [ns2] }),
- expect.any(Object),
- expect.objectContaining({ namespaces: [ns3] }),
+ { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj1.id}` }) },
+ expect.objectContaining({ namespaces: [ns2, ns3] }),
];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
@@ -649,24 +656,19 @@ describe('SavedObjectsRepository', () => {
).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"'));
});
- it(`returns error when initialNamespaces is used with a non-shareable object`, async () => {
- const test = async (objType) => {
- const obj = { ...obj3, type: objType, initialNamespaces: [] };
- await bulkCreateError(
+ it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => {
+ const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
obj,
- undefined,
- expectErrorResult(
- obj,
- createBadRequestError('"initialNamespaces" can only be used on multi-namespace types')
- )
- );
- };
- await test('dashboard');
- await test(NAMESPACE_AGNOSTIC_TYPE);
- await test(MULTI_NAMESPACE_ISOLATED_TYPE);
+ createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types')
+ )
+ );
});
- it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => {
+ it(`returns error when initialNamespaces is empty`, async () => {
const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] };
await bulkCreateError(
obj,
@@ -678,6 +680,26 @@ describe('SavedObjectsRepository', () => {
);
});
+ it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType, initialNamespaces) => {
+ const obj = { ...obj3, type: objType, initialNamespaces };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
+ obj,
+ createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
it(`returns error when type is invalid`, async () => {
const obj = { ...obj3, type: 'unknownType' };
await bulkCreateError(obj, undefined, expectErrorInvalidType(obj));
@@ -1865,12 +1887,46 @@ describe('SavedObjectsRepository', () => {
});
it(`adds initialNamespaces instead of namespace`, async () => {
- const options = { id, namespace, initialNamespaces: ['bar-namespace', 'baz-namespace'] };
- await createSuccess(MULTI_NAMESPACE_TYPE, attributes, options);
- expect(client.create).toHaveBeenCalledWith(
+ const ns2 = 'bar-namespace';
+ const ns3 = 'baz-namespace';
+ await savedObjectsRepository.create('dashboard', attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2],
+ });
+ await savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2],
+ });
+ await savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2, ns3],
+ });
+
+ expect(client.create).toHaveBeenCalledTimes(3);
+ expect(client.create).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ id: `${ns2}:dashboard:${id}`,
+ body: expect.objectContaining({ namespace: ns2 }),
+ }),
+ expect.anything()
+ );
+ expect(client.create).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`,
+ body: expect.objectContaining({ namespaces: [ns2] }),
+ }),
+ expect.anything()
+ );
+ expect(client.create).toHaveBeenNthCalledWith(
+ 3,
expect.objectContaining({
id: `${MULTI_NAMESPACE_TYPE}:${id}`,
- body: expect.objectContaining({ namespaces: options.initialNamespaces }),
+ body: expect.objectContaining({ namespaces: [ns2, ns3] }),
}),
expect.anything()
);
@@ -1892,29 +1948,40 @@ describe('SavedObjectsRepository', () => {
});
describe('errors', () => {
- it(`throws when options.initialNamespaces is used with a non-shareable object`, async () => {
- const test = async (objType) => {
- await expect(
- savedObjectsRepository.create(objType, attributes, { initialNamespaces: [namespace] })
- ).rejects.toThrowError(
- createBadRequestError(
- '"options.initialNamespaces" can only be used on multi-namespace types'
- )
- );
- };
- await test('dashboard');
- await test(MULTI_NAMESPACE_ISOLATED_TYPE);
- await test(NAMESPACE_AGNOSTIC_TYPE);
+ it(`throws when options.initialNamespaces is used with a space-agnostic object`, async () => {
+ await expect(
+ savedObjectsRepository.create(NAMESPACE_AGNOSTIC_TYPE, attributes, {
+ initialNamespaces: [namespace],
+ })
+ ).rejects.toThrowError(
+ createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types')
+ );
});
- it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => {
+ it(`throws when options.initialNamespaces is empty`, async () => {
await expect(
savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { initialNamespaces: [] })
).rejects.toThrowError(
- createBadRequestError('"options.initialNamespaces" must be a non-empty array of strings')
+ createBadRequestError('"initialNamespaces" must be a non-empty array of strings')
);
});
+ it(`throws when options.initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType, initialNamespaces) => {
+ await expect(
+ savedObjectsRepository.create(objType, attributes, { initialNamespaces })
+ ).rejects.toThrowError(
+ createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
it(`throws when options.namespace is '*'`, async () => {
await expect(
savedObjectsRepository.create(type, attributes, { namespace: ALL_NAMESPACES_STRING })
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index 1577f773434b9d..c9fa50da55df10 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -283,28 +283,18 @@ export class SavedObjectsRepository {
} = options;
const namespace = normalizeNamespace(options.namespace);
- if (initialNamespaces) {
- if (!this._registry.isShareable(type)) {
- throw SavedObjectsErrorHelpers.createBadRequestError(
- '"options.initialNamespaces" can only be used on multi-namespace types'
- );
- } else if (!initialNamespaces.length) {
- throw SavedObjectsErrorHelpers.createBadRequestError(
- '"options.initialNamespaces" must be a non-empty array of strings'
- );
- }
- }
+ this.validateInitialNamespaces(type, initialNamespaces);
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
}
const time = this._getCurrentTime();
- let savedObjectNamespace;
+ let savedObjectNamespace: string | undefined;
let savedObjectNamespaces: string[] | undefined;
- if (this._registry.isSingleNamespace(type) && namespace) {
- savedObjectNamespace = namespace;
+ if (this._registry.isSingleNamespace(type)) {
+ savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace;
} else if (this._registry.isMultiNamespace(type)) {
if (id && overwrite) {
// we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces
@@ -369,32 +359,29 @@ export class SavedObjectsRepository {
let bulkGetRequestIndexCounter = 0;
const expectedResults: Either[] = objects.map((object) => {
+ const { type, id, initialNamespaces } = object;
let error: DecoratedError | undefined;
- if (!this._allowedTypes.includes(object.type)) {
- error = SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type);
- } else if (object.initialNamespaces) {
- if (!this._registry.isShareable(object.type)) {
- error = SavedObjectsErrorHelpers.createBadRequestError(
- '"initialNamespaces" can only be used on multi-namespace types'
- );
- } else if (!object.initialNamespaces.length) {
- error = SavedObjectsErrorHelpers.createBadRequestError(
- '"initialNamespaces" must be a non-empty array of strings'
- );
+ if (!this._allowedTypes.includes(type)) {
+ error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
+ } else {
+ try {
+ this.validateInitialNamespaces(type, initialNamespaces);
+ } catch (e) {
+ error = e;
}
}
if (error) {
return {
tag: 'Left' as 'Left',
- error: { id: object.id, type: object.type, error: errorContent(error) },
+ error: { id, type, error: errorContent(error) },
};
}
- const method = object.id && overwrite ? 'index' : 'create';
- const requiresNamespacesCheck = object.id && this._registry.isMultiNamespace(object.type);
+ const method = id && overwrite ? 'index' : 'create';
+ const requiresNamespacesCheck = id && this._registry.isMultiNamespace(type);
- if (object.id == null) {
+ if (id == null) {
object.id = SavedObjectsUtils.generateId();
}
@@ -434,8 +421,8 @@ export class SavedObjectsRepository {
return expectedBulkGetResult;
}
- let savedObjectNamespace;
- let savedObjectNamespaces;
+ let savedObjectNamespace: string | undefined;
+ let savedObjectNamespaces: string[] | undefined;
let versionProperties;
const {
esRequestIndex,
@@ -469,7 +456,7 @@ export class SavedObjectsRepository {
versionProperties = getExpectedVersionProperties(version, actualResult);
} else {
if (this._registry.isSingleNamespace(object.type)) {
- savedObjectNamespace = namespace;
+ savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace;
} else if (this._registry.isMultiNamespace(object.type)) {
savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace);
}
@@ -2080,6 +2067,29 @@ export class SavedObjectsRepository {
const object = await this.get(type, id, options);
return { saved_object: object, outcome: 'exactMatch' };
}
+
+ private validateInitialNamespaces(type: string, initialNamespaces: string[] | undefined) {
+ if (!initialNamespaces) {
+ return;
+ }
+
+ if (this._registry.isNamespaceAgnostic(type)) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" cannot be used on space-agnostic types'
+ );
+ } else if (!initialNamespaces.length) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" must be a non-empty array of strings'
+ );
+ } else if (
+ !this._registry.isShareable(type) &&
+ (initialNamespaces.length > 1 || initialNamespaces.includes(ALL_NAMESPACES_STRING))
+ ) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ );
+ }
+ }
}
/**
diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts
index af682cfb81296e..1423050145695f 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.ts
@@ -63,7 +63,11 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
- * Note: this can only be used for multi-namespace object types.
+ * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
+ * including the "All spaces" identifier (`'*'`).
+ * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
+ * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+ * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
@@ -96,7 +100,11 @@ export interface SavedObjectsBulkCreateObject {
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
- * Note: this can only be used for multi-namespace object types.
+ * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
+ * including the "All spaces" identifier (`'*'`).
+ * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
+ * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+ * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 9e7721fde90e7d..fcecf39f7e53a9 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2901,7 +2901,7 @@ export class SavedObjectsRepository {
resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise;
-}
+ }
// @public
export interface SavedObjectsRepositoryFactory {
diff --git a/x-pack/package.json b/x-pack/package.json
index 01571cbb823fd6..1397a3da810722 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -28,8 +28,5 @@
"devDependencies": {
"@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers",
"@kbn/test": "link:../packages/kbn-test"
- },
- "dependencies": {
- "@kbn/ui-framework": "link:../packages/kbn-ui-framework"
}
}
\ No newline at end of file
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 3b91b07eb30f4e..16388b2faf52e1 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -1676,6 +1676,70 @@ describe('execute()', () => {
name: 'my name',
},
});
+
+ await expect(
+ actionsClient.execute({
+ actionId,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ },
+ ],
+ })
+ ).resolves.toMatchObject({ status: 'ok', actionId });
+
+ expect(actionExecutor.execute).toHaveBeenCalledWith({
+ actionId,
+ request,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ },
+ ],
+ });
+
+ await expect(
+ actionsClient.execute({
+ actionId,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ namespace: 'some-namespace',
+ },
+ ],
+ })
+ ).resolves.toMatchObject({ status: 'ok', actionId });
+
+ expect(actionExecutor.execute).toHaveBeenCalledWith({
+ actionId,
+ request,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ namespace: 'some-namespace',
+ },
+ ],
+ });
});
});
diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts
index 449d218ed5ae04..f8d13cdafa7557 100644
--- a/x-pack/plugins/actions/server/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client.ts
@@ -469,6 +469,7 @@ export class ActionsClient {
actionId,
params,
source,
+ relatedSavedObjects,
}: Omit): Promise> {
if (
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
@@ -476,7 +477,13 @@ export class ActionsClient {
) {
await this.authorization.ensureAuthorized('execute');
}
- return this.actionExecutor.execute({ actionId, params, source, request: this.request });
+ return this.actionExecutor.execute({
+ actionId,
+ params,
+ source,
+ request: this.request,
+ relatedSavedObjects,
+ });
}
public async enqueueExecution(options: EnqueueExecutionOptions): Promise {
diff --git a/x-pack/plugins/actions/server/constants/event_log.ts b/x-pack/plugins/actions/server/constants/event_log.ts
index 508709c8783ab7..9163a0d105ce8a 100644
--- a/x-pack/plugins/actions/server/constants/event_log.ts
+++ b/x-pack/plugins/actions/server/constants/event_log.ts
@@ -8,5 +8,6 @@
export const EVENT_LOG_PROVIDER = 'actions';
export const EVENT_LOG_ACTIONS = {
execute: 'execute',
+ executeStart: 'execute-start',
executeViaHttp: 'execute-via-http',
};
diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts
index 4cacba6dc880ab..ee8064d2aadc53 100644
--- a/x-pack/plugins/actions/server/create_execute_function.test.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.test.ts
@@ -83,6 +83,62 @@ describe('execute()', () => {
});
});
+ test('schedules the action with all given parameters and relatedSavedObjects', async () => {
+ const actionTypeRegistry = actionTypeRegistryMock.create();
+ const executeFn = createExecutionEnqueuerFunction({
+ taskManager: mockTaskManager,
+ actionTypeRegistry,
+ isESOCanEncrypt: true,
+ preconfiguredActions: [],
+ });
+ savedObjectsClient.get.mockResolvedValueOnce({
+ id: '123',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'mock-action',
+ },
+ references: [],
+ });
+ savedObjectsClient.create.mockResolvedValueOnce({
+ id: '234',
+ type: 'action_task_params',
+ attributes: {},
+ references: [],
+ });
+ await executeFn(savedObjectsClient, {
+ id: '123',
+ params: { baz: false },
+ spaceId: 'default',
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ source: asHttpRequestExecutionSource(request),
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ namespace: 'some-namespace',
+ type: 'some-type',
+ typeId: 'some-typeId',
+ },
+ ],
+ });
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ 'action_task_params',
+ {
+ actionId: '123',
+ params: { baz: false },
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ namespace: 'some-namespace',
+ type: 'some-type',
+ typeId: 'some-typeId',
+ },
+ ],
+ },
+ {}
+ );
+ });
+
test('schedules the action with all given parameters with a preconfigured action', async () => {
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts
index 4f3ffbef36c6e2..7dcd66c711bdde 100644
--- a/x-pack/plugins/actions/server/create_execute_function.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.ts
@@ -11,6 +11,7 @@ import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './ty
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects';
import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor';
import { isSavedObjectExecutionSource } from './lib';
+import { RelatedSavedObjects } from './lib/related_saved_objects';
interface CreateExecuteFunctionOptions {
taskManager: TaskManagerStartContract;
@@ -23,6 +24,7 @@ export interface ExecuteOptions extends Pick {
);
});
+test('writes to event log for execute and execute start', async () => {
+ const executorMock = setupActionExecutorMock();
+ executorMock.mockResolvedValue({
+ actionId: '1',
+ status: 'ok',
+ });
+ await actionExecutor.execute(executeParams);
+ expect(eventLogger.logEvent).toHaveBeenCalledTimes(2);
+ expect(eventLogger.logEvent.mock.calls[0][0]).toMatchObject({
+ event: {
+ action: 'execute-start',
+ },
+ kibana: {
+ saved_objects: [
+ {
+ rel: 'primary',
+ type: 'action',
+ id: '1',
+ type_id: 'test',
+ namespace: 'some-namespace',
+ },
+ ],
+ },
+ message: 'action started: test:1: action-1',
+ });
+ expect(eventLogger.logEvent.mock.calls[1][0]).toMatchObject({
+ event: {
+ action: 'execute',
+ },
+ kibana: {
+ saved_objects: [
+ {
+ rel: 'primary',
+ type: 'action',
+ id: '1',
+ type_id: 'test',
+ namespace: 'some-namespace',
+ },
+ ],
+ },
+ message: 'action executed: test:1: action-1',
+ });
+});
+
function setupActionExecutorMock() {
const actionType: jest.Mocked = {
id: 'test',
diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts
index 0737e0ce3f071d..e9e7b17288611b 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.ts
@@ -7,6 +7,7 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import { Logger, KibanaRequest } from 'src/core/server';
+import { cloneDeep } from 'lodash';
import { withSpan } from '@kbn/apm-utils';
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
import {
@@ -22,6 +23,7 @@ import { EVENT_LOG_ACTIONS } from '../constants/event_log';
import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
import { ActionsClient } from '../actions_client';
import { ActionExecutionSource } from './action_execution_source';
+import { RelatedSavedObjects } from './related_saved_objects';
export interface ActionExecutorContext {
logger: Logger;
@@ -42,6 +44,7 @@ export interface ExecuteOptions
+
+
+ >
+ )}
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle',
+ { defaultMessage: 'Active fields' }
+ )}
+
+ }
+ subtitle={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription',
+ { defaultMessage: 'Fields which belong to one or more engine.' }
)}
+ >
+
+
+
+ {hasConflicts && (
{i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle',
- { defaultMessage: 'Active fields' }
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle',
+ { defaultMessage: 'Inactive fields' }
)}
}
subtitle={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription',
- { defaultMessage: 'Fields which belong to one or more engine.' }
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription',
+ {
+ defaultMessage:
+ 'These fields have type conflicts. To activate these fields, change types in the source engines to match.',
+ }
)}
>
-
+
-
- {hasConflicts && (
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle',
- { defaultMessage: 'Inactive fields' }
- )}
-
- }
- subtitle={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription',
- {
- defaultMessage:
- 'These fields have type conflicts. To activate these fields, change types in the source engines to match.',
- }
- )}
- >
-
-
- )}
-
- >
+ )}
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
index 91ec8eda55fc36..cae16d70592faf 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
@@ -7,17 +7,18 @@
import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic';
import '../../../../__mocks__/shallow_useeffect.mock';
+import '../../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader, EuiButton } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
-import { Loading } from '../../../../shared/loading';
import { SchemaAddFieldModal } from '../../../../shared/schema';
+import { getPageHeaderActions } from '../../../../test_helpers';
-import { SchemaCallouts, SchemaTable, EmptyState } from '../components';
+import { SchemaCallouts, SchemaTable } from '../components';
import { Schema } from './';
@@ -56,27 +57,8 @@ describe('Schema', () => {
expect(actions.loadSchema).toHaveBeenCalled();
});
- it('renders a loading state', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow();
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
- it('renders an empty state', () => {
- setMockValues({ ...values, hasSchema: false });
- const wrapper = shallow();
-
- expect(wrapper.find(EmptyState)).toHaveLength(1);
- });
-
describe('page action buttons', () => {
- const subject = () =>
- shallow()
- .find(EuiPageHeader)
- .dive()
- .children()
- .dive();
+ const subject = () => getPageHeaderActions(shallow());
it('renders', () => {
const wrapper = subject();
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
index 7bc995b16468aa..d2a760e8accff3 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
@@ -9,14 +9,15 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import { EuiPageHeader, EuiButton, EuiPageContentBody } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../../shared/flash_messages';
-import { Loading } from '../../../../shared/loading';
import { SchemaAddFieldModal } from '../../../../shared/schema';
+import { getEngineBreadcrumbs } from '../../engine';
+import { AppSearchPageTemplate } from '../../layout';
import { SchemaCallouts, SchemaTable, EmptyState } from '../components';
+import { SCHEMA_TITLE } from '../constants';
import { SchemaLogic } from '../schema_logic';
export const Schema: React.FC = () => {
@@ -31,19 +32,18 @@ export const Schema: React.FC = () => {
loadSchema();
}, []);
- if (dataLoading) return ;
-
return (
- <>
- {
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.schema.updateSchemaButtonLabel',
- { defaultMessage: 'Update types' }
+ { defaultMessage: 'Save changes' }
)}
,
{
{ defaultMessage: 'Create a schema field' }
)}
,
- ]}
- />
-
-
-
- {hasSchema ? : }
- {isModalOpen && (
-
- )}
-
- >
+ ],
+ }}
+ isLoading={dataLoading}
+ isEmptyState={!hasSchema}
+ emptyState={}
+ >
+
+
+ {isModalOpen && (
+
+ )}
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx
index 902417d02665e6..ba9da900c01456 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx
@@ -10,6 +10,7 @@ import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
+ EuiCallOut,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
@@ -83,8 +84,13 @@ export const SchemaAddFieldModal: React.FC = ({
{ADD_FIELD_MODAL_TITLE}
- {ADD_FIELD_MODAL_DESCRIPTION}
-
+ {ADD_FIELD_MODAL_DESCRIPTION}}
+ />
+
diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md
index 032f77543acb97..ffbd20dd6f2bec 100644
--- a/x-pack/plugins/event_log/README.md
+++ b/x-pack/plugins/event_log/README.md
@@ -131,7 +131,7 @@ Below is a document in the expected structure, with descriptions of the fields:
instance_id: "alert instance id, for relevant documents",
action_group_id: "alert action group, for relevant documents",
action_subgroup: "alert action subgroup, for relevant documents",
- status: "overall alert status, after alert execution",
+ status: "overall alert status, after rule execution",
},
saved_objects: [
{
@@ -160,21 +160,26 @@ plugins:
- `action: execute-via-http` - generated when an action is executed via HTTP request
- `provider: alerting`
- - `action: execute` - generated when an alert executor runs
- - `action: execute-action` - generated when an alert schedules an action to run
- - `action: new-instance` - generated when an alert has a new instance id that is active
- - `action: recovered-instance` - generated when an alert has a previously active instance id that is no longer active
- - `action: active-instance` - generated when an alert determines an instance id is active
+ - `action: execute` - generated when a rule executor runs
+ - `action: execute-action` - generated when a rule schedules an action to run
+ - `action: new-instance` - generated when a rule has a new instance id that is active
+ - `action: recovered-instance` - generated when a rule has a previously active instance id that is no longer active
+ - `action: active-instance` - generated when a rule determines an instance id is active
For the `saved_objects` array elements, these are references to saved objects
-associated with the event. For the `alerting` provider, those are alert saved
-ojects and for the `actions` provider those are action saved objects. The
-`alerts:execute-action` event includes both the alert and action saved object
-references. For that event, only the alert reference has the optional `rel`
+associated with the event. For the `alerting` provider, those are rule saved
+ojects and for the `actions` provider those are connector saved objects. The
+`alerts:execute-action` event includes both the rule and connector saved object
+references. For that event, only the rule reference has the optional `rel`
property with a `primary` value. This property is used when searching the
event log to indicate which saved objects should be directly searchable via
-saved object references. For the `alerts:execute-action` event, searching
-only via the alert saved object reference will return the event.
+saved object references. For the `alerts:execute-action` event, only searching
+via the rule saved object reference will return the event; searching via the
+connector save object reference will **NOT** return the event. The
+`actions:execute` event also includes both the rule and connector saved object
+references, and both of them have the `rel` property with a `primary` value,
+allowing those events to be returned in searches of either the rule or
+connector.
## Event Log index - associated resources
diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts
deleted file mode 100644
index 94a049d10cc45d..00000000000000
--- a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { SavedObjectsClientContract } from 'kibana/server';
-import uuid from 'uuid';
-import { Version } from '@kbn/securitysolution-io-ts-types';
-import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
-import { getSavedObjectType } from '@kbn/securitysolution-list-utils';
-import {
- ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
- ENDPOINT_EVENT_FILTERS_LIST_ID,
- ENDPOINT_EVENT_FILTERS_LIST_NAME,
-} from '@kbn/securitysolution-list-constants';
-
-import { ExceptionListSoSchema } from '../../schemas/saved_objects';
-
-import { transformSavedObjectToExceptionList } from './utils';
-
-interface CreateEndpointEventFiltersListOptions {
- savedObjectsClient: SavedObjectsClientContract;
- user: string;
- tieBreaker?: string;
- version: Version;
-}
-
-/**
- * Creates the Endpoint Trusted Apps agnostic list if it does not yet exist
- *
- * @param savedObjectsClient
- * @param user
- * @param tieBreaker
- * @param version
- */
-export const createEndpointEventFiltersList = async ({
- savedObjectsClient,
- user,
- tieBreaker,
- version,
-}: CreateEndpointEventFiltersListOptions): Promise => {
- const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' });
- const dateNow = new Date().toISOString();
- try {
- const savedObject = await savedObjectsClient.create(
- savedObjectType,
- {
- comments: undefined,
- created_at: dateNow,
- created_by: user,
- description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
- entries: undefined,
- immutable: false,
- item_id: undefined,
- list_id: ENDPOINT_EVENT_FILTERS_LIST_ID,
- list_type: 'list',
- meta: undefined,
- name: ENDPOINT_EVENT_FILTERS_LIST_NAME,
- os_types: [],
- tags: [],
- tie_breaker_id: tieBreaker ?? uuid.v4(),
- type: 'endpoint_events',
- updated_by: user,
- version,
- },
- {
- // We intentionally hard coding the id so that there can only be one Event Filters list within the space
- id: ENDPOINT_EVENT_FILTERS_LIST_ID,
- }
- );
-
- return transformSavedObjectToExceptionList({ savedObject });
- } catch (err) {
- if (savedObjectsClient.errors.isConflictError(err)) {
- return null;
- } else {
- throw err;
- }
- }
-};
diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
index 4ccff2dd000b9b..77e82bf0f75783 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
@@ -54,7 +54,6 @@ import {
} from './find_exception_list_items';
import { createEndpointList } from './create_endpoint_list';
import { createEndpointTrustedAppsList } from './create_endpoint_trusted_apps_list';
-import { createEndpointEventFiltersList } from './create_endoint_event_filters_list';
export class ExceptionListClient {
private readonly user: string;
@@ -120,18 +119,6 @@ export class ExceptionListClient {
});
};
- /**
- * Create the Endpoint Event Filters Agnostic list if it does not yet exist (`null` is returned if it does exist)
- */
- public createEndpointEventFiltersList = async (): Promise => {
- const { savedObjectsClient, user } = this;
- return createEndpointEventFiltersList({
- savedObjectsClient,
- user,
- version: 1,
- });
- };
-
/**
* This is the same as "createListItem" except it applies specifically to the agnostic endpoint list and will
* auto-call the "createEndpointList" for you so that you have the best chance of the agnostic endpoint
diff --git a/x-pack/plugins/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts
index fa40cefcaed48d..74d32864385889 100644
--- a/x-pack/plugins/ml/common/types/results.ts
+++ b/x-pack/plugins/ml/common/types/results.ts
@@ -6,6 +6,7 @@
*/
import { estypes } from '@elastic/elasticsearch';
+import { LineAnnotationDatum, RectAnnotationDatum } from '@elastic/charts';
export interface GetStoppedPartitionResult {
jobs: string[] | Record;
@@ -13,6 +14,9 @@ export interface GetStoppedPartitionResult {
export interface GetDatafeedResultsChartDataResult {
bucketResults: number[][];
datafeedResults: number[][];
+ annotationResultsRect: RectAnnotationDatum[];
+ annotationResultsLine: LineAnnotationDatum[];
+ modelSnapshotResultsLine: LineAnnotationDatum[];
}
export interface DatafeedResultsChartDataParams {
diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
index afed7e79ff757f..b68e64a5d9f6ae 100644
--- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
+++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
@@ -494,13 +494,13 @@ class AnnotationsTableUI extends Component {
render: (annotation) => {
const viewDataFeedText = (
);
const viewDataFeedTooltipAriaLabelText = i18n.translate(
- 'xpack.ml.annotationsTable.viewDatafeedTooltipAriaLabel',
- { defaultMessage: 'View datafeed' }
+ 'xpack.ml.annotationsTable.datafeedChartTooltipAriaLabel',
+ { defaultMessage: 'Datafeed chart' }
);
return (
) : null}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts
index 71f3795518bc95..b3b94875231961 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 cf547a49cac4c3..2dece82e6f5c73 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 b514c8433daf48..d3856e6afa3982 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
@@ -7,26 +7,29 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
-
-import { EuiTabbedContent, EuiLoadingSpinner } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { EuiButtonIcon, EuiTabbedContent, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
import { extractJobDetails } from './extract_job_details';
import { JsonPane } from './json_tab';
import { DatafeedPreviewPane } from './datafeed_preview_tab';
import { AnnotationsTable } from '../../../../components/annotations/annotations_table';
+import { DatafeedModal } from '../datafeed_modal';
import { AnnotationFlyout } from '../../../../components/annotations/annotation_flyout';
import { ModelSnapshotTable } from '../../../../components/model_snapshots';
import { ForecastsTable } from './forecasts_table';
import { JobDetailsPane } from './job_details_pane';
import { JobMessagesPane } from './job_messages_pane';
-import { i18n } from '@kbn/i18n';
import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
export class JobDetailsUI extends Component {
constructor(props) {
super(props);
- this.state = {};
+ this.state = {
+ datafeedModalVisible: false,
+ };
if (this.props.addYourself) {
this.props.addYourself(props.jobId, (j) => this.updateJob(j));
}
@@ -77,6 +80,30 @@ export class JobDetailsUI extends Component {
alertRules,
} = extractJobDetails(job, basePath, refreshJobList);
+ datafeed.titleAction = (
+
+ }
+ >
+
+ this.setState({
+ datafeedModalVisible: true,
+ })
+ }
+ />
+
+ );
+
const tabs = [
{
id: 'job-settings',
@@ -105,6 +132,32 @@ export class JobDetailsUI extends Component {
/>
),
},
+ {
+ id: 'datafeed',
+ 'data-test-subj': 'mlJobListTab-datafeed',
+ name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', {
+ defaultMessage: 'Datafeed',
+ }),
+ content: (
+ <>
+
+ {this.props.jobId && this.state.datafeedModalVisible ? (
+ {
+ this.setState({
+ datafeedModalVisible: false,
+ });
+ }}
+ end={job.data_counts.latest_bucket_timestamp}
+ jobId={this.props.jobId}
+ />
+ ) : null}
+ >
+ ),
+ },
{
id: 'counts',
'data-test-subj': 'mlJobListTab-counts',
@@ -137,21 +190,6 @@ export class JobDetailsUI extends Component {
];
if (showFullDetails && datafeed.items.length) {
- // Datafeed should be at index 2 in tabs array for full details
- tabs.splice(2, 0, {
- id: 'datafeed',
- 'data-test-subj': 'mlJobListTab-datafeed',
- name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', {
- defaultMessage: 'Datafeed',
- }),
- content: (
-
- ),
- });
-
tabs.push(
{
id: 'datafeed-preview',
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
index 49d9bcde490520..4046f4d5d80712 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
@@ -9,6 +9,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import {
+ EuiFlexGroup,
+ EuiFlexItem,
EuiTitle,
EuiTable,
EuiTableBody,
@@ -42,9 +44,14 @@ function Section({ section }) {
return (
-
- {section.title}
-
+
+
+
+ {section.title}
+
+
+ {section.titleAction}
+
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
index 19ba5aa304bf04..25ef36782207f1 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
@@ -6,7 +6,10 @@
*/
// Service for obtaining data for the ML Results dashboards.
-import { GetStoppedPartitionResult } from '../../../../common/types/results';
+import {
+ GetStoppedPartitionResult,
+ GetDatafeedResultsChartDataResult,
+} from '../../../../common/types/results';
import { HttpService } from '../http_service';
import { basePath } from './index';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
@@ -148,7 +151,7 @@ export const resultsApiProvider = (httpService: HttpService) => ({
start,
end,
});
- return httpService.http({
+ return httpService.http({
path: `${basePath()}/results/datafeed_results_chart`,
method: 'POST',
body,
diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts
index 9413ee00184d20..81ee394b997044 100644
--- a/x-pack/plugins/ml/server/models/results_service/results_service.ts
+++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts
@@ -27,6 +27,7 @@ import {
import { MlJobsResponse } from '../../../common/types/job_service';
import type { MlClient } from '../../lib/ml_client';
import { datafeedsProvider } from '../job_service/datafeeds';
+import { annotationServiceProvider } from '../annotation_service';
// Service for carrying out Elasticsearch queries to obtain data for the
// ML Results dashboards.
@@ -620,13 +621,19 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
const finalResults: GetDatafeedResultsChartDataResult = {
bucketResults: [],
datafeedResults: [],
+ annotationResultsRect: [],
+ annotationResultsLine: [],
+ modelSnapshotResultsLine: [],
};
const { getDatafeedByJobId } = datafeedsProvider(client!, mlClient);
- const datafeedConfig = await getDatafeedByJobId(jobId);
- const { body: jobsResponse } = await mlClient.getJobs({ job_id: jobId });
- if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) {
+ const [datafeedConfig, { body: jobsResponse }] = await Promise.all([
+ getDatafeedByJobId(jobId),
+ mlClient.getJobs({ job_id: jobId }),
+ ]);
+
+ if (jobsResponse && (jobsResponse.count === 0 || jobsResponse.jobs === undefined)) {
throw Boom.notFound(`Job with the id "${jobId}" not found`);
}
@@ -696,10 +703,25 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
]) || [];
}
- const bucketResp = await mlClient.getBuckets({
- job_id: jobId,
- body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
- });
+ const { getAnnotations } = annotationServiceProvider(client!);
+
+ const [bucketResp, annotationResp, { body: modelSnapshotsResp }] = await Promise.all([
+ mlClient.getBuckets({
+ job_id: jobId,
+ body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
+ }),
+ getAnnotations({
+ jobIds: [jobId],
+ earliestMs: start,
+ latestMs: end,
+ maxAnnotations: 1000,
+ }),
+ mlClient.getModelSnapshots({
+ job_id: jobId,
+ start: String(start),
+ end: String(end),
+ }),
+ ]);
const bucketResults = bucketResp?.body?.buckets ?? [];
bucketResults.forEach((dataForTime) => {
@@ -708,6 +730,36 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
finalResults.bucketResults.push([timestamp, eventCount]);
});
+ const annotationResults = annotationResp.annotations[jobId] || [];
+ annotationResults.forEach((annotation) => {
+ const timestamp = Number(annotation?.timestamp);
+ const endTimestamp = Number(annotation?.end_timestamp);
+ if (timestamp === endTimestamp) {
+ finalResults.annotationResultsLine.push({
+ dataValue: timestamp,
+ details: annotation.annotation,
+ });
+ } else {
+ finalResults.annotationResultsRect.push({
+ coordinates: {
+ x0: timestamp,
+ x1: endTimestamp,
+ },
+ details: annotation.annotation,
+ });
+ }
+ });
+
+ const modelSnapshots = modelSnapshotsResp?.model_snapshots ?? [];
+ modelSnapshots.forEach((modelSnapshot) => {
+ const timestamp = Number(modelSnapshot?.timestamp);
+
+ finalResults.modelSnapshotResultsLine.push({
+ dataValue: timestamp,
+ details: modelSnapshot.description,
+ });
+ });
+
return finalResults;
}
diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx
index 8b4075ba67cdc7..44af8b33279753 100644
--- a/x-pack/plugins/monitoring/public/alerts/badge.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx
@@ -19,13 +19,18 @@ import { getAlertPanelsByCategory } from './lib/get_alert_panels_by_category';
import { getAlertPanelsByNode } from './lib/get_alert_panels_by_node';
export const numberOfAlertsLabel = (count: number) => `${count} alert${count > 1 ? 's' : ''}`;
+export const numberOfRulesLabel = (count: number) => `${count} rule${count > 1 ? 's' : ''}`;
const MAX_TO_SHOW_BY_CATEGORY = 8;
-const PANEL_TITLE = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
+const PANEL_TITLE_ALERTS = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
defaultMessage: 'Alerts',
});
+const PANEL_TITLE_RULES = i18n.translate('xpack.monitoring.rules.badge.panelTitle', {
+ defaultMessage: 'Rules',
+});
+
const GROUP_BY_NODE = i18n.translate('xpack.monitoring.alerts.badge.groupByNode', {
defaultMessage: 'Group by node',
});
@@ -54,6 +59,7 @@ export const AlertsBadge: React.FC = (props: Props) => {
const [showByNode, setShowByNode] = React.useState(
!inSetupMode && alertCount > MAX_TO_SHOW_BY_CATEGORY
);
+ const PANEL_TITLE = inSetupMode ? PANEL_TITLE_RULES : PANEL_TITLE_ALERTS;
React.useEffect(() => {
if (inSetupMode && showByNode) {
@@ -93,10 +99,12 @@ export const AlertsBadge: React.FC = (props: Props) => {
setShowPopover(true)}
>
- {numberOfAlertsLabel(alertCount)}
+ {inSetupMode ? numberOfRulesLabel(alertCount) : numberOfAlertsLabel(alertCount)}
);
diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx
index 29b17cd426c58b..fdd49ad17168de 100644
--- a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx
@@ -5,18 +5,7 @@
* 2.0.
*/
-import React from 'react';
import md5 from 'md5';
-import * as i18n from './translations';
-import { ErrorMessage } from './types';
-
-export const permissionsReadOnlyErrorMessage: ErrorMessage = {
- id: 'read-only-privileges-error',
- title: i18n.READ_ONLY_FEATURE_TITLE,
- description: <>{i18n.READ_ONLY_FEATURE_MSG}>,
- errorType: 'warning',
-};
-
export const createCalloutId = (ids: string[], delimiter: string = '|'): string =>
md5(ids.join(delimiter));
diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts
index cb7236b445be12..20bb57daf5841b 100644
--- a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts
+++ b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts
@@ -7,21 +7,6 @@
import { i18n } from '@kbn/i18n';
-export const READ_ONLY_FEATURE_TITLE = i18n.translate(
- 'xpack.observability.cases.readOnlyFeatureTitle',
- {
- defaultMessage: 'You cannot open new or update existing cases',
- }
-);
-
-export const READ_ONLY_FEATURE_MSG = i18n.translate(
- 'xpack.observability.cases.readOnlyFeatureDescription',
- {
- defaultMessage:
- 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.',
- }
-);
-
export const DISMISS_CALLOUT = i18n.translate(
'xpack.observability.cases.dismissErrorsPushServiceCallOutTitle',
{
diff --git a/x-pack/plugins/observability/public/components/app/cases/translations.ts b/x-pack/plugins/observability/public/components/app/cases/translations.ts
index 1a5abe218edf52..a85b0bc744e66a 100644
--- a/x-pack/plugins/observability/public/components/app/cases/translations.ts
+++ b/x-pack/plugins/observability/public/components/app/cases/translations.ts
@@ -201,3 +201,17 @@ export const CONNECTORS = i18n.translate('xpack.observability.cases.caseView.con
export const EDIT_CONNECTOR = i18n.translate('xpack.observability.cases.caseView.editConnector', {
defaultMessage: 'Change external incident management system',
});
+
+export const READ_ONLY_BADGE_TEXT = i18n.translate(
+ 'xpack.observability.cases.badge.readOnly.text',
+ {
+ defaultMessage: 'Read only',
+ }
+);
+
+export const READ_ONLY_BADGE_TOOLTIP = i18n.translate(
+ 'xpack.observability.cases.badge.readOnly.tooltip',
+ {
+ defaultMessage: 'Unable to create or edit cases',
+ }
+);
diff --git a/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx b/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx
new file mode 100644
index 00000000000000..4d8779e1ea150a
--- /dev/null
+++ b/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback, useEffect } from 'react';
+
+import * as i18n from '../components/app/cases/translations';
+import { useGetUserCasesPermissions } from '../hooks/use_get_user_cases_permissions';
+import { useKibana } from '../utils/kibana_react';
+
+/**
+ * This component places a read-only icon badge in the header if user only has read permissions
+ */
+export function useReadonlyHeader() {
+ const userPermissions = useGetUserCasesPermissions();
+ const chrome = useKibana().services.chrome;
+
+ // if the user is read only then display the glasses badge in the global navigation header
+ const setBadge = useCallback(() => {
+ if (userPermissions != null && !userPermissions.crud && userPermissions.read) {
+ chrome.setBadge({
+ text: i18n.READ_ONLY_BADGE_TEXT,
+ tooltip: i18n.READ_ONLY_BADGE_TOOLTIP,
+ iconType: 'glasses',
+ });
+ }
+ }, [chrome, userPermissions]);
+
+ useEffect(() => {
+ setBadge();
+
+ // remove the icon after the component unmounts
+ return () => {
+ chrome.setBadge();
+ };
+ }, [setBadge, chrome]);
+}
diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx
index f73f3b4cf57d75..442104a7106017 100644
--- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx
@@ -10,35 +10,28 @@ import React from 'react';
import { AllCases } from '../../components/app/cases/all_cases';
import * as i18n from '../../components/app/cases/translations';
-import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout';
import { CaseFeatureNoPermissions } from './feature_no_permissions';
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
import { usePluginContext } from '../../hooks/use_plugin_context';
+import { useReadonlyHeader } from '../../hooks/use_readonly_header';
import { casesBreadcrumbs } from './links';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
export const AllCasesPage = React.memo(() => {
const userPermissions = useGetUserCasesPermissions();
const { ObservabilityPageTemplate } = usePluginContext();
+ useReadonlyHeader();
useBreadcrumbs([casesBreadcrumbs.cases]);
return userPermissions == null || userPermissions?.read ? (
- <>
- {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
- )}
- {i18n.PAGE_TITLE}>,
- }}
- >
-
-
- >
+ {i18n.PAGE_TITLE}>,
+ }}
+ >
+
+
) : (
);
diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx
index 6adf5ad286808f..f93cb5c4e7919a 100644
--- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx
@@ -5,45 +5,35 @@
* 2.0.
*/
-import React from 'react';
+import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { CaseView } from '../../components/app/cases/case_view';
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
import { useKibana } from '../../utils/kibana_react';
import { CASES_APP_ID } from '../../components/app/cases/constants';
-import { CaseCallOut, permissionsReadOnlyErrorMessage } from '../../components/app/cases/callout';
+import { useReadonlyHeader } from '../../hooks/use_readonly_header';
export const CaseDetailsPage = React.memo(() => {
const {
application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
+ const casesUrl = getUrlForApp(CASES_APP_ID);
const userPermissions = useGetUserCasesPermissions();
const { detailName: caseId, subCaseId } = useParams<{
detailName?: string;
subCaseId?: string;
}>();
+ useReadonlyHeader();
- const casesUrl = getUrlForApp(CASES_APP_ID);
- if (userPermissions != null && !userPermissions.read) {
- navigateToUrl(casesUrl);
- return null;
- }
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, navigateToUrl, userPermissions]);
return caseId != null ? (
- <>
- {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
- )}
-
- >
+
) : null;
});
diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
index a4df4855b0204d..9676eb7eba1470 100644
--- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect } from 'react';
import styled from 'styled-components';
import { EuiButtonEmpty } from '@elastic/eui';
@@ -38,10 +38,12 @@ function ConfigureCasesPageComponent() {
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(getCaseUrl());
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]);
- if (userPermissions != null && !userPermissions.read) {
- navigateToUrl(casesUrl);
- return null;
- }
+
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, userPermissions, navigateToUrl]);
return (
{
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(getCaseUrl());
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]);
- if (userPermissions != null && !userPermissions.crud) {
- navigateToUrl(casesUrl);
- return null;
- }
+
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.crud) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, navigateToUrl, userPermissions]);
return (
+ <>
+
+
{errorBody}
-
+ >
);
}
return (
-
-
-
-
-
-
-
-
-
-
- {saveErrorFeedback}
-
-
+
+
+ }
+ />
-
+
+
+
+
+ {saveErrorFeedback}
+
+
+
+ {this.renderCurrentStep()}
- {this.renderCurrentStep()}
+
-
+ {this.renderNavigation()}
- {this.renderNavigation()}
-
{savingFeedback}
-
+
);
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
index 4fe1674e8c6436..5e97ff5e2980d3 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
@@ -195,7 +195,7 @@ export class DetailPanel extends Component {
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
index 16919b8388e2e4..e1f9ec2b3a315c 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
@@ -70,7 +70,7 @@ describe('', () => {
({ component, find, exists } = initTestBed({ isLoading: true }));
const loading = find('rollupJobDetailLoading');
expect(loading.length).toBeTruthy();
- expect(loading.text()).toEqual('Loading rollup job...');
+ expect(loading.text()).toEqual('Loading rollup job…');
// Make sure the title and the tabs are visible
expect(exists('detailPanelTabSelected')).toBeTruthy();
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
index 589546a11ef38e..b2448eb6107742 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
@@ -12,24 +12,19 @@ import { i18n } from '@kbn/i18n';
import {
EuiButton,
+ EuiButtonEmpty,
EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
+ EuiPageHeader,
EuiPageContent,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
EuiSpacer,
- EuiText,
- EuiTextColor,
- EuiTitle,
- EuiCallOut,
} from '@elastic/eui';
import { withKibana } from '../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { getRouterLinkProps, listBreadcrumb } from '../../services';
+import { documentationLinks } from '../../services/documentation_links';
+
import { JobTable } from './job_table';
import { DetailPanel } from './detail_panel';
@@ -87,38 +82,26 @@ export class JobListUi extends Component {
this.props.closeDetailPanel();
}
- getHeaderSection() {
- return (
-
-
-
-
-
-
-
- );
- }
-
renderNoPermission() {
const title = i18n.translate('xpack.rollupJobs.jobList.noPermissionTitle', {
defaultMessage: 'Permission error',
});
return (
-
- {this.getHeaderSection()}
-
-
+
-
-
-
+ iconType="alert"
+ title={{title}
}
+ body={
+
+
+
+ }
+ />
+
);
}
@@ -130,101 +113,110 @@ export class JobListUi extends Component {
const title = i18n.translate('xpack.rollupJobs.jobList.loadingErrorTitle', {
defaultMessage: 'Error loading rollup jobs',
});
+
return (
-
- {this.getHeaderSection()}
-
-
- {statusCode} {errorString}
-
-
+
+ {title}}
+ body={
+
+ {statusCode} {errorString}
+
+ }
+ />
+
);
}
renderEmpty() {
return (
-
-
-
- }
- body={
-
-
+
+
+
+ }
+ body={
+
+
+
+
+
+ }
+ actions={
+
+
-
-
- }
- actions={
-
-
-
- }
- />
+
+ }
+ />
+
);
}
renderLoading() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
renderList() {
- const { isLoading } = this.props;
-
return (
-
-
- {this.getHeaderSection()}
-
-
-
+ <>
+
+
+
+ }
+ rightSideItems={[
+
-
-
-
+ ,
+ ]}
+ />
- {isLoading ? this.renderLoading() : }
+
+
+
-
+ >
);
}
@@ -241,15 +233,13 @@ export class JobListUi extends Component {
}
} else if (!isLoading && !hasJobs) {
content = this.renderEmpty();
+ } else if (isLoading) {
+ content = this.renderLoading();
} else {
content = this.renderList();
}
- return (
-
- {content}
-
- );
+ return content;
}
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
index 3283f4f521fc0e..b2c738a033b3cb 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
@@ -22,6 +22,15 @@ jest.mock('../../services', () => {
};
});
+jest.mock('../../services/documentation_links', () => {
+ const coreMocks = jest.requireActual('../../../../../../../src/core/public/mocks');
+
+ return {
+ init: jest.fn(),
+ documentationLinks: coreMocks.docLinksServiceMock.createStartContract().links,
+ };
+});
+
const defaultProps = {
history: { location: {} },
loadJobs: () => {},
@@ -52,14 +61,14 @@ describe('', () => {
it('should display a loading message when loading the jobs', () => {
const { component, exists } = initTestBed({ isLoading: true });
- expect(exists('jobListLoading')).toBeTruthy();
+ expect(exists('sectionLoading')).toBeTruthy();
expect(component.find('JobTable').length).toBeFalsy();
});
it('should display the when there are jobs', () => {
const { component, exists } = initTestBed({ hasJobs: true });
- expect(exists('jobListLoading')).toBeFalsy();
+ expect(exists('sectionLoading')).toBeFalsy();
expect(component.find('JobTable').length).toBeTruthy();
});
@@ -71,21 +80,20 @@ describe('', () => {
},
});
- it('should display a callout with the status and the message', () => {
+ it('should display an error with the status and the message', () => {
expect(exists('jobListError')).toBeTruthy();
expect(find('jobListError').find('EuiText').text()).toEqual('400 Houston we got a problem.');
});
});
describe('when the user does not have the permission to access it', () => {
- const { exists } = initTestBed({ jobLoadError: { status: 403 } });
+ const { exists, find } = initTestBed({ jobLoadError: { status: 403 } });
- it('should render a callout message', () => {
+ it('should render an error message', () => {
expect(exists('jobListNoPermission')).toBeTruthy();
- });
-
- it('should display the page header', () => {
- expect(exists('jobListPageHeader')).toBeTruthy();
+ expect(find('jobListNoPermission').find('EuiText').text()).toEqual(
+ 'You do not have permission to view or add rollup jobs.'
+ );
});
});
});
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
index fe3d2cbd4cbe0d..83135cf219f350 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -28,10 +28,11 @@ import {
EuiTableRowCellCheckbox,
EuiText,
EuiToolTip,
+ EuiButton,
} from '@elastic/eui';
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../common';
-import { METRIC_TYPE } from '../../../services';
+import { METRIC_TYPE, getRouterLinkProps } from '../../../services';
import { trackUiMetric } from '../../../../kibana_services';
import { JobActionMenu, JobStatus } from '../../components';
@@ -346,9 +347,9 @@ export class JobTable extends Component {
const atLeastOneItemSelected = Object.keys(idToSelectedJobMap).length > 0;
return (
-
-
- {atLeastOneItemSelected ? (
+
+
+ {atLeastOneItemSelected && (
- ) : null}
+ )}
+
+
+
+
+
@@ -409,7 +418,7 @@ export class JobTable extends Component {
{jobs.length > 0 ? this.renderPager() : null}
-
+
);
}
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
index 3fa879923c40ab..d52f3fa35a5441 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
@@ -20,6 +20,14 @@ jest.mock('../../../../kibana_services', () => {
};
});
+jest.mock('../../../services', () => {
+ const services = jest.requireActual('../../../services');
+ return {
+ ...services,
+ getRouterLinkProps: (link) => ({ href: link }),
+ };
+});
+
const defaultProps = {
jobs: [],
pager: new Pager(20, 10, 1),
diff --git a/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
index 0dc3a02d3c0779..c63d01f3c200d5 100644
--- a/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
+++ b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
@@ -5,9 +5,7 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
-
-import { loadJobs as sendLoadJobsRequest, deserializeJobs, showApiError } from '../../services';
+import { loadJobs as sendLoadJobsRequest, deserializeJobs } from '../../services';
import { LOAD_JOBS_START, LOAD_JOBS_SUCCESS, LOAD_JOBS_FAILURE } from '../action_types';
export const loadJobs = () => async (dispatch) => {
@@ -19,17 +17,10 @@ export const loadJobs = () => async (dispatch) => {
try {
jobs = await sendLoadJobsRequest();
} catch (error) {
- dispatch({
+ return dispatch({
type: LOAD_JOBS_FAILURE,
payload: { error },
});
-
- return showApiError(
- error,
- i18n.translate('xpack.rollupJobs.loadAction.errorTitle', {
- defaultMessage: 'Error loading rollup jobs',
- })
- );
}
dispatch({
diff --git a/x-pack/plugins/rollup/public/shared_imports.ts b/x-pack/plugins/rollup/public/shared_imports.ts
index fd281753186665..c8d7f1d9f13f3d 100644
--- a/x-pack/plugins/rollup/public/shared_imports.ts
+++ b/x-pack/plugins/rollup/public/shared_imports.ts
@@ -5,4 +5,8 @@
* 2.0.
*/
-export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public';
+export {
+ extractQueryParams,
+ indices,
+ SectionLoading,
+} from '../../../../src/plugins/es_ui_shared/public';
diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
index fa1a786bc8a71d..46ddfbcfc2de55 100644
--- a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
+++ b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
@@ -5,10 +5,10 @@
* 2.0.
*/
-import { getRouter, setHttp } from '../../crud_app/services';
+import { getRouter, setHttp, init as initDocumentation } from '../../crud_app/services';
import { mockHttpRequest, pageHelpers, nextTick } from './helpers';
import { JOBS } from './helpers/constants';
-import { coreMock } from '../../../../../../src/core/public/mocks';
+import { coreMock, docLinksServiceMock } from '../../../../../../src/core/public/mocks';
jest.mock('../../crud_app/services', () => {
const services = jest.requireActual('../../crud_app/services');
@@ -38,6 +38,7 @@ describe('', () => {
beforeAll(() => {
startMock = coreMock.createStart();
setHttp(startMock.http);
+ initDocumentation(docLinksServiceMock.createStartContract());
});
beforeEach(async () => {
diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
index cfb63893ee423a..3987e18538e577 100644
--- a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
+++ b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
@@ -24,6 +24,15 @@ jest.mock('../../kibana_services', () => {
};
});
+jest.mock('../../crud_app/services/documentation_links', () => {
+ const coreMocks = jest.requireActual('../../../../../../src/core/public/mocks');
+
+ return {
+ init: jest.fn(),
+ documentationLinks: coreMocks.docLinksServiceMock.createStartContract().links,
+ };
+});
+
const { setup } = pageHelpers.jobList;
describe('Smoke test cloning an existing rollup job from job list', () => {
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 4ce20af28b1d72..e65ff1afcc9c33 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -70,6 +70,9 @@ export enum SecurityPageName {
administration = 'administration',
}
+/**
+ * The ID of the cases plugin
+ */
export const CASES_APP_ID = `${APP_ID}:${SecurityPageName.case}`;
export const APP_OVERVIEW_PATH = `${APP_PATH}/overview`;
diff --git a/x-pack/plugins/security_solution/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 bdf2ab96600ea7..932f1ceac61e81 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/public/cases/components/callout/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx
index 29b17cd426c58b..fdd49ad17168de 100644
--- a/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/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/security_solution/public/cases/components/callout/translations.ts b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts
index db4809126452f9..617995cc366b06 100644
--- a/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts
@@ -7,21 +7,6 @@
import { i18n } from '@kbn/i18n';
-export const READ_ONLY_FEATURE_TITLE = i18n.translate(
- 'xpack.securitySolution.cases.readOnlyFeatureTitle',
- {
- defaultMessage: 'You cannot open new or update existing cases',
- }
-);
-
-export const READ_ONLY_FEATURE_MSG = i18n.translate(
- 'xpack.securitySolution.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.securitySolution.cases.dismissErrorsPushServiceCallOutTitle',
{
diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx
index 77fa9e8b3cc8c9..02047c774ca6f5 100644
--- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx
@@ -200,7 +200,7 @@ describe('AddToCaseAction', () => {
).toBeTruthy();
});
- it('disabled when user does not have crud permissions', () => {
+ it('hides the icon when user does not have crud permissions', () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
crud: false,
read: true,
@@ -212,8 +212,6 @@ describe('AddToCaseAction', () => {
);
- expect(
- wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('isDisabled')
- ).toBeTruthy();
+ expect(wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).exists()).toBeFalsy();
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx
index eaad912a4dc51c..7025bff1ce49a6 100644
--- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx
@@ -208,19 +208,21 @@ const AddToCaseActionComponent: React.FC = ({
return (
<>
-
-
-
-
-
+ {userCanCrud && (
+
+
+
+
+
+ )}
{isCreateCaseFlyoutOpen && (
{
return userPermissions == null || userPermissions?.read ? (
<>
- {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
- )}
diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
index 73077334268626..a086409e55df52 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { SecurityPageName } from '../../app/types';
@@ -16,7 +16,6 @@ import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
import { getCaseUrl } from '../../common/components/link_to';
import { navTabs } from '../../app/home/home_navigations';
import { CaseView } from '../components/case_view';
-import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../components/callout';
import { CASES_APP_ID } from '../../../common/constants';
export const CaseDetailsPage = React.memo(() => {
@@ -30,20 +29,15 @@ export const CaseDetailsPage = React.memo(() => {
}>();
const search = useGetUrlSearch(navTabs.case);
- if (userPermissions != null && !userPermissions.read) {
- navigateToApp(CASES_APP_ID, { path: getCaseUrl(search) });
- return null;
- }
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToApp(CASES_APP_ID, { path: getCaseUrl(search) });
+ }
+ }, [navigateToApp, userPermissions, search]);
return caseId != null ? (
<>
- {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
- )}
{
[search]
);
- if (userPermissions != null && !userPermissions.read) {
- navigateToApp(CASES_APP_ID, { path: getCaseUrl(search) });
- return null;
- }
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToApp(CASES_APP_ID, {
+ path: getCaseUrl(search),
+ });
+ }
+ }, [navigateToApp, userPermissions, search]);
const HeaderWrapper = styled.div`
padding-top: ${({ theme }) => theme.eui.paddingSizes.l};
diff --git a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx
index 19f97bae60ebe9..3c5197f19eff12 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo } from 'react';
import { SecurityPageName } from '../../app/types';
import { getCaseUrl } from '../../common/components/link_to';
@@ -25,6 +25,7 @@ export const CreateCasePage = React.memo(() => {
const {
application: { navigateToApp },
} = useKibana().services;
+
const backOptions = useMemo(
() => ({
href: getCaseUrl(search),
@@ -34,12 +35,13 @@ export const CreateCasePage = React.memo(() => {
[search]
);
- if (userPermissions != null && !userPermissions.crud) {
- navigateToApp(CASES_APP_ID, {
- path: getCaseUrl(search),
- });
- return null;
- }
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.crud) {
+ navigateToApp(CASES_APP_ID, {
+ path: getCaseUrl(search),
+ });
+ }
+ }, [userPermissions, navigateToApp, search]);
return (
<>
diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.test.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.test.tsx
new file mode 100644
index 00000000000000..0d12d63fdc244c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/pages/index.test.tsx
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+import { BrowserRouter as Router } from 'react-router-dom';
+
+import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
+import { TestProviders } from '../../common/mock';
+import { Case } from '.';
+
+const useKibanaMock = useKibana as jest.Mocked;
+jest.mock('../../common/lib/kibana');
+
+const mockedSetBadge = jest.fn();
+
+describe('CaseContainerComponent', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ useKibanaMock().services.chrome.setBadge = mockedSetBadge;
+ });
+
+ it('does not display the readonly glasses badge when the user has write permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: true,
+ read: false,
+ });
+
+ mount(
+
+
+
+
+
+ );
+
+ expect(mockedSetBadge).not.toBeCalled();
+ });
+
+ it('does not display the readonly glasses badge when the user has neither write nor read permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: false,
+ });
+
+ mount(
+
+
+
+
+
+ );
+
+ expect(mockedSetBadge).not.toBeCalled();
+ });
+
+ it('does not display the readonly glasses badge when the user has null permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue(null);
+
+ mount(
+
+
+
+
+
+ );
+
+ expect(mockedSetBadge).not.toBeCalled();
+ });
+
+ it('displays the readonly glasses badge read permissions but not write', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: true,
+ });
+
+ mount(
+
+
+
+
+
+ );
+
+ expect(mockedSetBadge).toBeCalledTimes(1);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx
index 314bdc9bfd117f..fca19cf5c70a7e 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx
@@ -5,13 +5,15 @@
* 2.0.
*/
-import React from 'react';
-
+import React, { useEffect } from 'react';
import { Route, Switch } from 'react-router-dom';
+
+import * as i18n from './translations';
import { CaseDetailsPage } from './case_details';
import { CasesPage } from './case';
import { CreateCasePage } from './create_case';
import { ConfigureCasesPage } from './configure_cases';
+import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
const casesPagePath = '';
const caseDetailsPagePath = `${casesPagePath}/:detailName`;
@@ -21,30 +23,51 @@ const subCaseDetailsPagePathWithCommentId = `${subCaseDetailsPagePath}/:commentI
const createCasePagePath = `${casesPagePath}/create`;
const configureCasesPagePath = `${casesPagePath}/configure`;
-const CaseContainerComponent: React.FC = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
+const CaseContainerComponent: React.FC = () => {
+ const userPermissions = useGetUserCasesPermissions();
+ const chrome = useKibana().services.chrome;
+
+ useEffect(() => {
+ // if the user is read only then display the glasses badge in the global navigation header
+ if (userPermissions != null && !userPermissions.crud && userPermissions.read) {
+ chrome.setBadge({
+ text: i18n.READ_ONLY_BADGE_TEXT,
+ tooltip: i18n.READ_ONLY_BADGE_TOOLTIP,
+ iconType: 'glasses',
+ });
+ }
+
+ // remove the icon after the component unmounts
+ return () => {
+ chrome.setBadge();
+ };
+ }, [userPermissions, chrome]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
export const Case = React.memo(CaseContainerComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/pages/translations.ts b/x-pack/plugins/security_solution/public/cases/pages/translations.ts
index 1a811a3fd7bbc3..6768401b3f608e 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/translations.ts
+++ b/x-pack/plugins/security_solution/public/cases/pages/translations.ts
@@ -157,3 +157,24 @@ export const GO_TO_DOCUMENTATION = i18n.translate(
export const CONNECTORS = i18n.translate('xpack.securitySolution.cases.caseView.connectors', {
defaultMessage: 'External Incident Management System',
});
+
+export const EDIT_CONNECTOR = i18n.translate(
+ 'xpack.securitySolution.cases.caseView.editConnector',
+ {
+ defaultMessage: 'Change external incident management system',
+ }
+);
+
+export const READ_ONLY_BADGE_TEXT = i18n.translate(
+ 'xpack.securitySolution.cases.badge.readOnly.text',
+ {
+ defaultMessage: 'Read only',
+ }
+);
+
+export const READ_ONLY_BADGE_TOOLTIP = i18n.translate(
+ 'xpack.securitySolution.cases.badge.readOnly.tooltip',
+ {
+ defaultMessage: 'Unable to create or edit cases',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx
new file mode 100644
index 00000000000000..96a7eacb7fb08c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { useGetUserCasesPermissions } from '../../../common/lib/kibana';
+import { TestProviders } from '../../../common/mock';
+import { HeaderGlobal } from '.';
+
+jest.mock('../../../common/lib/kibana');
+
+describe('HeaderGlobal', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('does not display the cases tab when the user does not have read permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: false,
+ });
+
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find(`[data-test-subj="navigation-case"]`).exists()).toBeFalsy();
+ });
+
+ it('displays the cases tab when the user has read permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: true,
+ read: true,
+ });
+
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find(`[data-test-subj="navigation-case"]`).exists()).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
index 4a7ac8a148f647..e91905183aab10 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
@@ -19,7 +19,7 @@ import { MlPopover } from '../ml_popover/ml_popover';
import { SiemNavigation } from '../navigation';
import * as i18n from './translations';
import { useGetUrlSearch } from '../navigation/use_get_url_search';
-import { useKibana } from '../../lib/kibana';
+import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana';
import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants';
import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal';
import { LinkAnchor } from '../links';
@@ -91,6 +91,18 @@ export const HeaderGlobal = React.memo(
},
[navigateToApp, search]
);
+
+ const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
+
+ // build a list of tabs to exclude
+ const tabsToExclude = new Set([
+ ...(hideDetectionEngine ? [SecurityPageName.detections] : []),
+ ...(!hasCasesReadPermissions ? [SecurityPageName.case] : []),
+ ]);
+
+ // include the tab if it is not in the set of excluded ones
+ const tabsToDisplay = pickBy((_, key) => !tabsToExclude.has(key), navTabs);
+
return (
@@ -109,14 +121,7 @@ export const HeaderGlobal = React.memo(
- key !== SecurityPageName.detections, navTabs)
- : navTabs
- }
- />
+
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
index 42d53f97d478b4..ef311a7ca43b17 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
@@ -41,27 +41,27 @@ export const HostIsolationPanel = React.memo(
return findAlertId ? findAlertId[0] : '';
}, [details]);
- const { caseIds } = useCasesFromAlerts({ alertId });
+ const { casesInfo } = useCasesFromAlerts({ alertId });
// Cases related components to be used in both isolate and unisolate actions from the alert details flyout entry point
- const caseCount: number = useMemo(() => caseIds.length, [caseIds]);
+ const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);
const casesList = useMemo(
() =>
- caseIds.map((id, index) => {
+ casesInfo.map((caseInfo, index) => {
return (
-
-
+
+
);
}),
- [caseIds]
+ [casesInfo]
);
const associatedCases = useMemo(() => {
@@ -90,7 +90,7 @@ export const HostIsolationPanel = React.memo(
endpointId={endpointId}
hostName={hostName}
cases={associatedCases}
- caseIds={caseIds}
+ casesInfo={casesInfo}
cancelCallback={cancelCallback}
/>
) : (
@@ -98,7 +98,7 @@ export const HostIsolationPanel = React.memo(
endpointId={endpointId}
hostName={hostName}
cases={associatedCases}
- caseIds={caseIds}
+ casesInfo={casesInfo}
cancelCallback={cancelCallback}
/>
);
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx
index afc2951e26e1fe..b209c2f9c6e24e 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx
@@ -15,24 +15,29 @@ import {
EndpointIsolateForm,
EndpointIsolateSuccess,
} from '../../../common/components/endpoint/host_isolation';
+import { CasesFromAlertsResponse } from '../../containers/detection_engine/alerts/types';
export const IsolateHost = React.memo(
({
endpointId,
hostName,
cases,
- caseIds,
+ casesInfo,
cancelCallback,
}: {
endpointId: string;
hostName: string;
cases: ReactNode;
- caseIds: string[];
+ casesInfo: CasesFromAlertsResponse;
cancelCallback: () => void;
}) => {
const [comment, setComment] = useState('');
const [isIsolated, setIsIsolated] = useState(false);
+ const caseIds: string[] = casesInfo.map((caseInfo): string => {
+ return caseInfo.id;
+ });
+
const { loading, isolateHost } = useHostIsolation({ endpointId, comment, caseIds });
const confirmHostIsolation = useCallback(async () => {
@@ -47,7 +52,7 @@ export const IsolateHost = React.memo(
[]
);
- const caseCount: number = useMemo(() => caseIds.length, [caseIds]);
+ const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);
const hostIsolatedSuccess = useMemo(() => {
return (
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx
index 71f7cadda2f68c..ad8e8eaddb39e3 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx
@@ -15,24 +15,29 @@ import {
EndpointUnisolateForm,
} from '../../../common/components/endpoint/host_isolation';
import { useHostUnisolation } from '../../containers/detection_engine/alerts/use_host_unisolation';
+import { CasesFromAlertsResponse } from '../../containers/detection_engine/alerts/types';
export const UnisolateHost = React.memo(
({
endpointId,
hostName,
cases,
- caseIds,
+ casesInfo,
cancelCallback,
}: {
endpointId: string;
hostName: string;
cases: ReactNode;
- caseIds: string[];
+ casesInfo: CasesFromAlertsResponse;
cancelCallback: () => void;
}) => {
const [comment, setComment] = useState('');
const [isUnIsolated, setIsUnIsolated] = useState(false);
+ const caseIds: string[] = casesInfo.map((caseInfo): string => {
+ return caseInfo.id;
+ });
+
const { loading, unIsolateHost } = useHostUnisolation({ endpointId, comment, caseIds });
const confirmHostUnIsolation = useCallback(async () => {
@@ -47,7 +52,7 @@ export const UnisolateHost = React.memo(
[]
);
- const caseCount: number = useMemo(() => caseIds.length, [caseIds]);
+ const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);
const hostUnisolatedSuccess = useMemo(() => {
return (
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
index 69358958a395cd..e4bddfba8278bb 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
@@ -1046,6 +1046,6 @@ export const mockHostIsolation: HostIsolationResponse = {
};
export const mockCaseIdsFromAlertId: CasesFromAlertsResponse = [
- '818601a0-b26b-11eb-8759-6b318e8cf4bc',
- '8a774850-b26b-11eb-8759-6b318e8cf4bc',
+ { id: '818601a0-b26b-11eb-8759-6b318e8cf4bc', title: 'Case 1' },
+ { id: '8a774850-b26b-11eb-8759-6b318e8cf4bc', title: 'Case 2' },
];
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
index 52b477d95076b6..54d4b6fdcbafdb 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
@@ -48,7 +48,7 @@ export interface AlertsIndex {
index_mapping_outdated: boolean;
}
-export type CasesFromAlertsResponse = string[];
+export type CasesFromAlertsResponse = Array<{ id: string; title: string }>;
export interface Privilege {
username: string;
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
index 0867fb001051a1..00aa7c9baa9aca 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
@@ -35,7 +35,7 @@ describe('useCasesFromAlerts hook', () => {
expect(spyOnCases).toHaveBeenCalledTimes(1);
expect(result.current).toEqual({
loading: false,
- caseIds: mockCaseIdsFromAlertId,
+ casesInfo: mockCaseIdsFromAlertId,
});
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
index 85b80a588e88d2..eeb7968d6b2f27 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
@@ -15,7 +15,7 @@ import { CasesFromAlertsResponse } from './types';
interface CasesFromAlertsStatus {
loading: boolean;
- caseIds: CasesFromAlertsResponse;
+ casesInfo: CasesFromAlertsResponse;
}
export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromAlertsStatus => {
@@ -48,5 +48,5 @@ export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromA
isMounted = false;
};
}, [alertId, addError]);
- return { loading, caseIds: cases };
+ return { loading, casesInfo: cases };
};
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx
index 996835296fcc4b..cb7733e3049850 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx
@@ -13,7 +13,7 @@ import {
getCreateCaseUrl,
} from '../../../common/components/link_to/redirect_to_case';
import { useFormatUrl } from '../../../common/components/link_to';
-import { useKibana } from '../../../common/lib/kibana';
+import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
import { APP_ID, CASES_APP_ID } from '../../../../common/constants';
import { SecurityPageName } from '../../../app/types';
import { AllCasesNavProps } from '../../../cases/components/all_cases';
@@ -26,6 +26,8 @@ const RecentCasesComponent = () => {
application: { navigateToApp },
} = useKibana().services;
+ const hasWritePermissions = useGetUserCasesPermissions()?.crud ?? false;
+
return casesUi.getRecentCases({
allCasesNavigation: {
href: formatUrl(getCaseUrl()),
@@ -60,6 +62,7 @@ const RecentCasesComponent = () => {
});
},
},
+ hasWritePermissions,
maxCasesToShow: MAX_CASES_TO_SHOW,
owner: [APP_ID],
});
diff --git a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx
new file mode 100644
index 00000000000000..76c5663644a789
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.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 { mount } from 'enzyme';
+import { waitFor } from '@testing-library/react';
+import { TestProviders } from '../../../common/mock';
+import { Sidebar } from './sidebar';
+import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
+import { casesPluginMock } from '../../../../../cases/public/mocks';
+import { CasesUiStart } from '../../../../../cases/public';
+
+jest.mock('../../../common/lib/kibana');
+
+const useKibanaMock = useKibana as jest.MockedFunction;
+
+describe('Sidebar', () => {
+ let casesMock: jest.Mocked;
+
+ beforeEach(() => {
+ casesMock = casesPluginMock.createStartContract();
+ casesMock.getRecentCases.mockImplementation(() => <>{'test'}>);
+ useKibanaMock.mockReturnValue(({
+ services: {
+ cases: casesMock,
+ application: {
+ // these are needed by the RecentCases component if it is rendered.
+ navigateToApp: jest.fn(),
+ getUrlForApp: jest.fn(() => ''),
+ },
+ },
+ } as unknown) as ReturnType);
+ });
+
+ it('does not render the recently created cases section when the user does not have read permissions', async () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: false,
+ });
+
+ await waitFor(() =>
+ mount(
+
+ {}} />
+
+ )
+ );
+
+ expect(casesMock.getRecentCases).not.toHaveBeenCalled();
+ });
+
+ it('does render the recently created cases section when the user has read permissions', async () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: true,
+ });
+
+ await waitFor(() =>
+ mount(
+
+ {}} />
+
+ )
+ );
+
+ expect(casesMock.getRecentCases).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx
index 77cfa220f07220..b8701f3ef1639d 100644
--- a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx
@@ -18,6 +18,7 @@ import { SidebarHeader } from '../../../common/components/sidebar_header';
import * as i18n from '../../pages/translations';
import { RecentCases } from '../recent_cases';
+import { useGetUserCasesPermissions } from '../../../common/lib/kibana';
const SidebarFlexGroup = styled(EuiFlexGroup)`
width: 305px;
@@ -46,13 +47,20 @@ export const Sidebar = React.memo<{
[recentTimelinesFilterBy, setRecentTimelinesFilterBy]
);
+ // only render the recently created cases view if the user has at least read permissions
+ const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
+
return (
-
-
-
+ {hasCasesReadPermissions && (
+ <>
+
+
+
-
+
+ >
+ )}
{recentTimelinesFilters}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx
index 68b4f2e4a0c31c..206fcb2dc087ca 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
-import { useKibana } from '../../../../common/lib/kibana';
+import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana';
import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock';
import { TimelineId } from '../../../../../common/types/timeline';
import { useTimelineKpis } from '../../../containers/kpis';
@@ -57,7 +57,7 @@ const defaultMocks = {
loading: false,
selectedPatterns: mockIndexNames,
};
-describe('Timeline KPIs', () => {
+describe('header', () => {
const mount = useMountAppended();
beforeEach(() => {
@@ -75,86 +75,124 @@ describe('Timeline KPIs', () => {
jest.clearAllMocks();
});
- describe('when the data is not loading and the response contains data', () => {
+ describe('AddToCaseButton', () => {
beforeEach(() => {
mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]);
});
- it('renders the component, labels and values succesfully', async () => {
+
+ it('renders the button when the user has write permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: true,
+ read: false,
+ });
+
const wrapper = mount(
);
- expect(wrapper.find('[data-test-subj="siem-timeline-kpis"]').exists()).toEqual(true);
- // label
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining('Processes')
- );
- // value
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining('1')
- );
- });
- });
- describe('when the data is loading', () => {
- beforeEach(() => {
- mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]);
+ expect(wrapper.find('[data-test-subj="attach-timeline-case-button"]').exists()).toBeTruthy();
});
- it('renders a loading indicator for values', async () => {
+
+ it('does not render the button when the user does not have write permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: false,
+ });
+
const wrapper = mount(
);
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining('--')
- );
+
+ expect(wrapper.find('[data-test-subj="attach-timeline-case-button"]').exists()).toBeFalsy();
});
});
- describe('when the response is null and timeline is blank', () => {
- beforeEach(() => {
- mockUseTimelineKpis.mockReturnValue([false, null]);
+ describe('Timeline KPIs', () => {
+ describe('when the data is not loading and the response contains data', () => {
+ beforeEach(() => {
+ mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]);
+ });
+ it('renders the component, labels and values successfully', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-kpis"]').exists()).toEqual(true);
+ // label
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('Processes')
+ );
+ // value
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('1')
+ );
+ });
});
- it('renders labels and the default empty string', async () => {
- const wrapper = mount(
-
-
-
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining('Processes')
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining(getEmptyValue())
- );
+ describe('when the data is loading', () => {
+ beforeEach(() => {
+ mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]);
+ });
+ it('renders a loading indicator for values', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('--')
+ );
+ });
});
- });
- describe('when the response contains numbers larger than one thousand', () => {
- beforeEach(() => {
- mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]);
+ describe('when the response is null and timeline is blank', () => {
+ beforeEach(() => {
+ mockUseTimelineKpis.mockReturnValue([false, null]);
+ });
+ it('renders labels and the default empty string', async () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('Processes')
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining(getEmptyValue())
+ );
+ });
});
- it('formats the numbers correctly', async () => {
- const wrapper = mount(
-
-
-
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining('1k')
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-user-kpi"]').first().text()).toEqual(
- expect.stringContaining('1m')
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-source-ip-kpi"]').first().text()).toEqual(
- expect.stringContaining('1b')
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-host-kpi"]').first().text()).toEqual(
- expect.stringContaining('999')
- );
+
+ describe('when the response contains numbers larger than one thousand', () => {
+ beforeEach(() => {
+ mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]);
+ });
+ it('formats the numbers correctly', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('1k')
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-user-kpi"]').first().text()).toEqual(
+ expect.stringContaining('1m')
+ );
+ expect(
+ wrapper.find('[data-test-subj="siem-timeline-source-ip-kpi"]').first().text()
+ ).toEqual(expect.stringContaining('1b'));
+ expect(wrapper.find('[data-test-subj="siem-timeline-host-kpi"]').first().text()).toEqual(
+ expect.stringContaining('999')
+ );
+ });
});
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
index dd8cdb818cad75..216282b72920c5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
@@ -35,7 +35,7 @@ import { TimerangeInput } from '../../../../../common/search_strategy';
import { AddToCaseButton } from '../add_to_case_button';
import { AddTimelineButton } from '../add_timeline_button';
import { SaveTimelineButton } from '../../timeline/header/save_timeline_button';
-import { useKibana } from '../../../../common/lib/kibana';
+import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
import { InspectButton } from '../../../../common/components/inspect';
import { useTimelineKpis } from '../../../containers/kpis';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
@@ -319,6 +319,8 @@ const FlyoutHeaderComponent: React.FC = ({ timelineId }) => {
filterQuery: combinedQueries?.filterQuery ?? '',
});
+ const hasWritePermissions = useGetUserCasesPermissions()?.crud ?? false;
+
return (
@@ -350,9 +352,11 @@ const FlyoutHeaderComponent: React.FC = ({ timelineId }) => {
-
-
-
+ {hasWritePermissions && (
+
+
+
+ )}
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
index f5d3b30bf15faa..e27a09efd97108 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
@@ -140,8 +140,6 @@ export async function getEndpointEventFiltersList(
policyId ? ` or exception-list-agnostic.attributes.tags:\"policy:${policyId}\"` : ''
})`;
- await eClient.createEndpointEventFiltersList();
-
return getFilteredEndpointExceptionList(
eClient,
schemaVersion,
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
index c54c12981c7710..50fe2ffe2cea99 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
@@ -10,6 +10,7 @@ import { RequestHandler } from 'src/core/server';
import uuid from 'uuid';
import { TypeOf } from '@kbn/config-schema';
import { CommentType } from '../../../../../cases/common';
+import { CasesByAlertId } from '../../../../../cases/common/api/cases/case';
import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions';
import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/endpoint/constants';
import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common';
@@ -103,12 +104,17 @@ export const isolationRequestHandler = function (
let caseIDs: string[] = req.body.case_ids?.slice() || [];
if (req.body.alert_ids && req.body.alert_ids.length > 0) {
const newIDs: string[][] = await Promise.all(
- req.body.alert_ids.map(async (a: string) =>
- (await endpointContext.service.getCasesClient(req)).cases.getCaseIDsByAlertID({
+ req.body.alert_ids.map(async (a: string) => {
+ const cases: CasesByAlertId = await (
+ await endpointContext.service.getCasesClient(req)
+ ).cases.getCasesByAlertID({
alertID: a,
options: { owner: APP_ID },
- })
- )
+ });
+ return cases.map((caseInfo): string => {
+ return caseInfo.id;
+ });
+ })
);
caseIDs = caseIDs.concat(...newIDs);
}
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 7e4d0989af413d..ac9d854f18211f 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -129,17 +129,23 @@ export interface PluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PluginStart {}
-const securitySubPlugins = [
+const casesSubPlugin = `${APP_ID}:${SecurityPageName.case}`;
+
+/**
+ * Don't include cases here so that the sub feature can govern whether Cases is enabled in the navigation
+ */
+const securitySubPluginsNoCases = [
APP_ID,
`${APP_ID}:${SecurityPageName.overview}`,
`${APP_ID}:${SecurityPageName.detections}`,
`${APP_ID}:${SecurityPageName.hosts}`,
`${APP_ID}:${SecurityPageName.network}`,
`${APP_ID}:${SecurityPageName.timelines}`,
- `${APP_ID}:${SecurityPageName.case}`,
`${APP_ID}:${SecurityPageName.administration}`,
];
+const allSecuritySubPlugins = [...securitySubPluginsNoCases, casesSubPlugin];
+
export class Plugin implements IPlugin {
private readonly logger: Logger;
private readonly config: ConfigType;
@@ -305,7 +311,7 @@ export class Plugin implements IPlugin await testSubjects.exists('canvasWorkpadLoaderTable')
+ async () => await testSubjects.exists('canvasWorkpadTable')
);
await a11y.testAppSnapshot();
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts
index f7d7c1df8fd46b..5c578d2d08daee 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts
@@ -519,47 +519,93 @@ export default function ({ getService }: FtrProviderContext) {
type: 'action',
id: connectorId,
provider: 'actions',
- actions: new Map([['execute', { equal: 1 }]]),
- filter: 'event.action:(execute)',
+ actions: new Map([
+ ['execute-start', { equal: 1 }],
+ ['execute', { equal: 1 }],
+ ]),
+ // filter: 'event.action:(execute)',
});
});
- const event = events[0];
+ const startExecuteEvent = events[0];
+ const executeEvent = events[1];
- const duration = event?.event?.duration;
- const eventStart = Date.parse(event?.event?.start || 'undefined');
- const eventEnd = Date.parse(event?.event?.end || 'undefined');
+ const duration = executeEvent?.event?.duration;
+ const executeEventStart = Date.parse(executeEvent?.event?.start || 'undefined');
+ const startExecuteEventStart = Date.parse(startExecuteEvent?.event?.start || 'undefined');
+ const executeEventEnd = Date.parse(executeEvent?.event?.end || 'undefined');
const dateNow = Date.now();
expect(typeof duration).to.be('number');
- expect(eventStart).to.be.ok();
- expect(eventEnd).to.be.ok();
+ expect(executeEventStart).to.be.ok();
+ expect(startExecuteEventStart).to.equal(executeEventStart);
+ expect(executeEventEnd).to.be.ok();
const durationDiff = Math.abs(
- Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
+ Math.round(duration! / NANOS_IN_MILLIS) - (executeEventEnd - executeEventStart)
);
// account for rounding errors
expect(durationDiff < 1).to.equal(true);
- expect(eventStart <= eventEnd).to.equal(true);
- expect(eventEnd <= dateNow).to.equal(true);
+ expect(executeEventStart <= executeEventEnd).to.equal(true);
+ expect(executeEventEnd <= dateNow).to.equal(true);
- expect(event?.event?.outcome).to.equal(outcome);
+ expect(executeEvent?.event?.outcome).to.equal(outcome);
- expect(event?.kibana?.saved_objects).to.eql([
+ expect(executeEvent?.kibana?.saved_objects).to.eql([
{
rel: 'primary',
type: 'action',
id: connectorId,
+ namespace: 'space1',
type_id: actionTypeId,
- namespace: spaceId,
},
]);
+ expect(startExecuteEvent?.kibana?.saved_objects).to.eql(executeEvent?.kibana?.saved_objects);
- expect(event?.message).to.eql(message);
+ expect(executeEvent?.message).to.eql(message);
+ expect(startExecuteEvent?.message).to.eql(message.replace('executed', 'started'));
if (errorMessage) {
- expect(event?.error?.message).to.eql(errorMessage);
+ expect(executeEvent?.error?.message).to.eql(errorMessage);
}
+
+ // const event = events[0];
+
+ // const duration = event?.event?.duration;
+ // const eventStart = Date.parse(event?.event?.start || 'undefined');
+ // const eventEnd = Date.parse(event?.event?.end || 'undefined');
+ // const dateNow = Date.now();
+
+ // expect(typeof duration).to.be('number');
+ // expect(eventStart).to.be.ok();
+ // expect(eventEnd).to.be.ok();
+
+ // const durationDiff = Math.abs(
+ // Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
+ // );
+
+ // // account for rounding errors
+ // expect(durationDiff < 1).to.equal(true);
+ // expect(eventStart <= eventEnd).to.equal(true);
+ // expect(eventEnd <= dateNow).to.equal(true);
+
+ // expect(event?.event?.outcome).to.equal(outcome);
+
+ // expect(event?.kibana?.saved_objects).to.eql([
+ // {
+ // rel: 'primary',
+ // type: 'action',
+ // id: connectorId,
+ // type_id: actionTypeId,
+ // namespace: spaceId,
+ // },
+ // ]);
+
+ // expect(event?.message).to.eql(message);
+
+ // if (errorMessage) {
+ // expect(event?.error?.message).to.eql(errorMessage);
+ // }
}
}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts
index 147b6abfb88d14..d494c99c80e8f9 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts
@@ -100,6 +100,7 @@ export default function ({ getService }: FtrProviderContext) {
actionTypeId: 'test.index-record',
outcome: 'success',
message: `action executed: test.index-record:${createdAction.id}: My action`,
+ startMessage: `action started: test.index-record:${createdAction.id}: My action`,
});
});
@@ -336,10 +337,19 @@ export default function ({ getService }: FtrProviderContext) {
outcome: string;
message: string;
errorMessage?: string;
+ startMessage?: string;
}
async function validateEventLog(params: ValidateEventLogParams): Promise {
- const { spaceId, actionId, actionTypeId, outcome, message, errorMessage } = params;
+ const {
+ spaceId,
+ actionId,
+ actionTypeId,
+ outcome,
+ message,
+ startMessage,
+ errorMessage,
+ } = params;
const events: IValidatedEvent[] = await retry.try(async () => {
return await getEventLog({
@@ -348,33 +358,39 @@ export default function ({ getService }: FtrProviderContext) {
type: 'action',
id: actionId,
provider: 'actions',
- actions: new Map([['execute', { equal: 1 }]]),
+ actions: new Map([
+ ['execute-start', { equal: 1 }],
+ ['execute', { equal: 1 }],
+ ]),
});
});
- const event = events[0];
+ const startExecuteEvent = events[0];
+ const executeEvent = events[1];
- const duration = event?.event?.duration;
- const eventStart = Date.parse(event?.event?.start || 'undefined');
- const eventEnd = Date.parse(event?.event?.end || 'undefined');
+ const duration = executeEvent?.event?.duration;
+ const executeEventStart = Date.parse(executeEvent?.event?.start || 'undefined');
+ const startExecuteEventStart = Date.parse(startExecuteEvent?.event?.start || 'undefined');
+ const executeEventEnd = Date.parse(executeEvent?.event?.end || 'undefined');
const dateNow = Date.now();
expect(typeof duration).to.be('number');
- expect(eventStart).to.be.ok();
- expect(eventEnd).to.be.ok();
+ expect(executeEventStart).to.be.ok();
+ expect(startExecuteEventStart).to.equal(executeEventStart);
+ expect(executeEventEnd).to.be.ok();
const durationDiff = Math.abs(
- Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
+ Math.round(duration! / NANOS_IN_MILLIS) - (executeEventEnd - executeEventStart)
);
// account for rounding errors
expect(durationDiff < 1).to.equal(true);
- expect(eventStart <= eventEnd).to.equal(true);
- expect(eventEnd <= dateNow).to.equal(true);
+ expect(executeEventStart <= executeEventEnd).to.equal(true);
+ expect(executeEventEnd <= dateNow).to.equal(true);
- expect(event?.event?.outcome).to.equal(outcome);
+ expect(executeEvent?.event?.outcome).to.equal(outcome);
- expect(event?.kibana?.saved_objects).to.eql([
+ expect(executeEvent?.kibana?.saved_objects).to.eql([
{
rel: 'primary',
type: 'action',
@@ -383,11 +399,15 @@ export default function ({ getService }: FtrProviderContext) {
type_id: actionTypeId,
},
]);
+ expect(startExecuteEvent?.kibana?.saved_objects).to.eql(executeEvent?.kibana?.saved_objects);
- expect(event?.message).to.eql(message);
+ expect(executeEvent?.message).to.eql(message);
+ if (startMessage) {
+ expect(startExecuteEvent?.message).to.eql(startMessage);
+ }
if (errorMessage) {
- expect(event?.error?.message).to.eql(errorMessage);
+ expect(executeEvent?.error?.message).to.eql(errorMessage);
}
}
}
diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts
index 63be1736405fc1..921589b2341dd8 100644
--- a/x-pack/test/case_api_integration/common/lib/utils.ts
+++ b/x-pack/test/case_api_integration/common/lib/utils.ts
@@ -46,6 +46,7 @@ import {
CasesConfigurationsResponse,
CaseUserActionsResponse,
AlertResponse,
+ CasesByAlertId,
} from '../../../../plugins/cases/common/api';
import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock';
import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers';
@@ -1017,7 +1018,7 @@ export const findCases = async ({
return res;
};
-export const getCaseIDsByAlert = async ({
+export const getCasesByAlert = async ({
supertest,
alertID,
query = {},
@@ -1029,7 +1030,7 @@ export const getCaseIDsByAlert = async ({
query?: Record;
expectedHttpCode?: number;
auth?: { user: User; space: string | null };
-}): Promise => {
+}): Promise => {
const { body: res } = await supertest
.get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/alerts/${alertID}`)
.auth(auth.user.username, auth.user.password)
diff --git a/x-pack/test/case_api_integration/common/lib/validation.ts b/x-pack/test/case_api_integration/common/lib/validation.ts
new file mode 100644
index 00000000000000..8b1c8ca1241493
--- /dev/null
+++ b/x-pack/test/case_api_integration/common/lib/validation.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+
+import { CaseResponse, CasesByAlertId } from '../../../../plugins/cases/common';
+
+/**
+ * Ensure that the result of the alerts API request matches with the cases created for the test.
+ */
+export function validateCasesFromAlertIDResponse(
+ casesFromAPIResponse: CasesByAlertId,
+ createdCasesForTest: CaseResponse[]
+) {
+ const idToTitle = new Map(
+ createdCasesForTest.map((caseInfo) => [caseInfo.id, caseInfo.title])
+ );
+
+ for (const apiResCase of casesFromAPIResponse) {
+ // check that the title in the api response matches the title in the map from the created cases
+ expect(apiResCase.title).to.be(idToTitle.get(apiResCase.id));
+ }
+}
diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts
index e34f879e3aff84..136e52d08f46ab 100644
--- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts
+++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts
@@ -13,9 +13,10 @@ import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/
import {
createCase,
createComment,
- getCaseIDsByAlert,
+ getCasesByAlert,
deleteAllCaseItems,
} from '../../../../common/lib/utils';
+import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation';
import { CaseResponse } from '../../../../../../plugins/cases/common';
import {
globalRead,
@@ -41,9 +42,9 @@ export default ({ getService }: FtrProviderContext): void => {
it('should return all cases with the same alert ID attached to them', async () => {
const [case1, case2, case3] = await Promise.all([
- createCase(supertest, getPostCaseRequest()),
- createCase(supertest, getPostCaseRequest()),
- createCase(supertest, getPostCaseRequest()),
+ createCase(supertest, getPostCaseRequest({ title: 'a' })),
+ createCase(supertest, getPostCaseRequest({ title: 'b' })),
+ createCase(supertest, getPostCaseRequest({ title: 'c' })),
]);
await Promise.all([
@@ -52,12 +53,10 @@ export default ({ getService }: FtrProviderContext): void => {
createComment({ supertest, caseId: case3.id, params: postCommentAlertReq }),
]);
- const caseIDsWithAlert = await getCaseIDsByAlert({ supertest, alertID: 'test-id' });
+ const caseIDsWithAlert = await getCasesByAlert({ supertest, alertID: 'test-id' });
expect(caseIDsWithAlert.length).to.eql(3);
- expect(caseIDsWithAlert).to.contain(case1.id);
- expect(caseIDsWithAlert).to.contain(case2.id);
- expect(caseIDsWithAlert).to.contain(case3.id);
+ validateCasesFromAlertIDResponse(caseIDsWithAlert, [case1, case2, case3]);
});
it('should return all cases with the same alert ID when more than 100 cases', async () => {
@@ -80,13 +79,11 @@ export default ({ getService }: FtrProviderContext): void => {
await Promise.all(commentPromises);
- const caseIDsWithAlert = await getCaseIDsByAlert({ supertest, alertID: 'test-id' });
+ const caseIDsWithAlert = await getCasesByAlert({ supertest, alertID: 'test-id' });
expect(caseIDsWithAlert.length).to.eql(numCases);
- for (const caseInfo of cases) {
- expect(caseIDsWithAlert).to.contain(caseInfo.id);
- }
+ validateCasesFromAlertIDResponse(caseIDsWithAlert, cases);
});
it('should return no cases when the alert ID is not found', async () => {
@@ -102,7 +99,7 @@ export default ({ getService }: FtrProviderContext): void => {
createComment({ supertest, caseId: case3.id, params: postCommentAlertReq }),
]);
- const caseIDsWithAlert = await getCaseIDsByAlert({ supertest, alertID: 'test-id100' });
+ const caseIDsWithAlert = await getCasesByAlert({ supertest, alertID: 'test-id100' });
expect(caseIDsWithAlert.length).to.eql(0);
});
@@ -120,7 +117,7 @@ export default ({ getService }: FtrProviderContext): void => {
createComment({ supertest, caseId: case3.id, params: postCommentAlertReq }),
]);
- const caseIDsWithAlert = await getCaseIDsByAlert({
+ const caseIDsWithAlert = await getCasesByAlert({
supertest,
alertID: 'test-id',
query: { owner: 'not-real' },
@@ -137,7 +134,7 @@ export default ({ getService }: FtrProviderContext): void => {
describe('rbac', () => {
const supertestWithoutAuth = getService('supertestWithoutAuth');
- it('should return the correct case IDs', async () => {
+ it('should return the correct cases info', async () => {
const secOnlyAuth = { user: secOnly, space: 'space1' };
const obsOnlyAuth = { user: obsOnly, space: 'space1' };
@@ -176,20 +173,20 @@ export default ({ getService }: FtrProviderContext): void => {
for (const scenario of [
{
user: globalRead,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
{
user: superUser,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
- { user: secOnlyRead, caseIDs: [case1.id, case2.id] },
- { user: obsOnlyRead, caseIDs: [case3.id] },
+ { user: secOnlyRead, cases: [case1, case2] },
+ { user: obsOnlyRead, cases: [case3] },
{
user: obsSecRead,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
]) {
- const res = await getCaseIDsByAlert({
+ const res = await getCasesByAlert({
supertest: supertestWithoutAuth,
// cast because the official type is string | string[] but the ids will always be a single value in the tests
alertID: postCommentAlertReq.alertId as string,
@@ -198,10 +195,9 @@ export default ({ getService }: FtrProviderContext): void => {
space: 'space1',
},
});
- expect(res.length).to.eql(scenario.caseIDs.length);
- for (const caseID of scenario.caseIDs) {
- expect(res).to.contain(caseID);
- }
+ expect(res.length).to.eql(scenario.cases.length);
+
+ validateCasesFromAlertIDResponse(res, scenario.cases);
}
});
@@ -224,7 +220,7 @@ export default ({ getService }: FtrProviderContext): void => {
auth: { user: superUser, space: scenario.space },
});
- await getCaseIDsByAlert({
+ await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: { user: scenario.user, space: scenario.space },
@@ -260,17 +256,17 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const res = await getCaseIDsByAlert({
+ const res = await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth,
query: { owner: 'securitySolutionFixture' },
});
- expect(res).to.eql([case1.id]);
+ expect(res).to.eql([{ id: case1.id, title: case1.title }]);
});
- it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => {
+ it('should return the correct cases info when the owner query parameter contains unprivileged values', async () => {
const auth = { user: obsSec, space: 'space1' };
const [case1, case2] = await Promise.all([
createCase(supertestWithoutAuth, getPostCaseRequest(), 200, auth),
@@ -297,7 +293,7 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const res = await getCaseIDsByAlert({
+ const res = await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: { user: secOnly, space: 'space1' },
@@ -305,7 +301,7 @@ export default ({ getService }: FtrProviderContext): void => {
query: { owner: ['securitySolutionFixture', 'observabilityFixture'] },
});
- expect(res).to.eql([case1.id]);
+ expect(res).to.eql([{ id: case1.id, title: case1.title }]);
});
});
});
diff --git a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts
index 9575bd99112f6b..f55427d13b32bf 100644
--- a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts
+++ b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts
@@ -12,7 +12,7 @@ import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/
import {
createCase,
createComment,
- getCaseIDsByAlert,
+ getCasesByAlert,
deleteAllCaseItems,
} from '../../../../common/lib/utils';
import {
@@ -30,6 +30,7 @@ import {
superUserDefaultSpaceAuth,
obsSecDefaultSpaceAuth,
} from '../../../utils';
+import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
@@ -43,7 +44,7 @@ export default ({ getService }: FtrProviderContext): void => {
const supertestWithoutAuth = getService('supertestWithoutAuth');
- it('should return the correct case IDs', async () => {
+ it('should return the correct cases info', async () => {
const [case1, case2, case3] = await Promise.all([
createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth),
createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth),
@@ -79,20 +80,20 @@ export default ({ getService }: FtrProviderContext): void => {
for (const scenario of [
{
user: globalRead,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
{
user: superUser,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
- { user: secOnlyReadSpacesAll, caseIDs: [case1.id, case2.id] },
- { user: obsOnlyReadSpacesAll, caseIDs: [case3.id] },
+ { user: secOnlyReadSpacesAll, cases: [case1, case2] },
+ { user: obsOnlyReadSpacesAll, cases: [case3] },
{
user: obsSecReadSpacesAll,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
]) {
- const res = await getCaseIDsByAlert({
+ const cases = await getCasesByAlert({
supertest: supertestWithoutAuth,
// cast because the official type is string | string[] but the ids will always be a single value in the tests
alertID: postCommentAlertReq.alertId as string,
@@ -101,10 +102,9 @@ export default ({ getService }: FtrProviderContext): void => {
space: null,
},
});
- expect(res.length).to.eql(scenario.caseIDs.length);
- for (const caseID of scenario.caseIDs) {
- expect(res).to.contain(caseID);
- }
+
+ expect(cases.length).to.eql(scenario.cases.length);
+ validateCasesFromAlertIDResponse(cases, scenario.cases);
}
});
@@ -123,7 +123,7 @@ export default ({ getService }: FtrProviderContext): void => {
auth: superUserDefaultSpaceAuth,
});
- await getCaseIDsByAlert({
+ await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: { user: noKibanaPrivileges, space: null },
@@ -157,7 +157,7 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- await getCaseIDsByAlert({
+ await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: { user: obsSecSpacesAll, space: 'space1' },
@@ -192,17 +192,17 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const res = await getCaseIDsByAlert({
+ const cases = await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: obsSecDefaultSpaceAuth,
query: { owner: 'securitySolutionFixture' },
});
- expect(res).to.eql([case1.id]);
+ expect(cases).to.eql([{ id: case1.id, title: case1.title }]);
});
- it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => {
+ it('should return the correct cases info when the owner query parameter contains unprivileged values', async () => {
const [case1, case2] = await Promise.all([
createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth),
createCase(
@@ -228,7 +228,7 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const res = await getCaseIDsByAlert({
+ const cases = await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: secOnlyDefaultSpaceAuth,
@@ -236,7 +236,7 @@ export default ({ getService }: FtrProviderContext): void => {
query: { owner: ['securitySolutionFixture', 'observabilityFixture'] },
});
- expect(res).to.eql([case1.id]);
+ expect(cases).to.eql([{ id: case1.id, title: case1.title }]);
});
});
};
diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts
index 9587502fb642ce..739f8e5ec08926 100644
--- a/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts
+++ b/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts
@@ -12,10 +12,11 @@ import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/
import {
createCase,
createComment,
- getCaseIDsByAlert,
+ getCasesByAlert,
deleteAllCaseItems,
getAuthWithSuperUser,
} from '../../../../common/lib/utils';
+import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
@@ -57,16 +58,14 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const caseIDsWithAlert = await getCaseIDsByAlert({
+ const cases = await getCasesByAlert({
supertest,
alertID: 'test-id',
auth: authSpace1,
});
- expect(caseIDsWithAlert.length).to.eql(3);
- expect(caseIDsWithAlert).to.contain(case1.id);
- expect(caseIDsWithAlert).to.contain(case2.id);
- expect(caseIDsWithAlert).to.contain(case3.id);
+ expect(cases.length).to.eql(3);
+ validateCasesFromAlertIDResponse(cases, [case1, case2, case3]);
});
it('should return 1 case in space2 when 2 cases were created in space1 and 1 in space2', async () => {
@@ -97,14 +96,14 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const caseIDsWithAlert = await getCaseIDsByAlert({
+ const casesByAlert = await getCasesByAlert({
supertest,
alertID: 'test-id',
auth: authSpace2,
});
- expect(caseIDsWithAlert.length).to.eql(1);
- expect(caseIDsWithAlert).to.eql([case3.id]);
+ expect(casesByAlert.length).to.eql(1);
+ expect(casesByAlert).to.eql([{ id: case3.id, title: case3.title }]);
});
});
};
diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js
index 5280ad0118fbac..fcc04aafdbcd85 100644
--- a/x-pack/test/functional/apps/canvas/smoke_test.js
+++ b/x-pack/test/functional/apps/canvas/smoke_test.js
@@ -17,7 +17,7 @@ export default function canvasSmokeTest({ getService, getPageObjects }) {
describe('smoke test', function () {
this.tags('includeFirefox');
- const workpadListSelector = 'canvasWorkpadLoaderTable > canvasWorkpadLoaderWorkpad';
+ const workpadListSelector = 'canvasWorkpadTable > canvasWorkpadTableWorkpad';
const testWorkpadId = 'workpad-1705f884-6224-47de-ba49-ca224fe6ec31';
before(async () => {
diff --git a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts
index d27f1acdd3e317..f2d78369bafee0 100644
--- a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts
+++ b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts
@@ -138,11 +138,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it(`landing page shows disabled "Create new case" button`, async () => {
await PageObjects.common.navigateToActualUrl('observabilityCases');
- await PageObjects.observability.expectCreateCaseButtonDisabled();
+ await PageObjects.observability.expectCreateCaseButtonMissing();
});
- it(`shows read-only callout`, async () => {
- await PageObjects.observability.expectReadOnlyCallout();
+ it(`shows read-only glasses badge`, async () => {
+ await PageObjects.observability.expectReadOnlyGlassesBadge();
});
it(`does not allow a case to be created`, async () => {
@@ -151,7 +151,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
// expect redirection to observability cases landing
- await PageObjects.observability.expectCreateCaseButtonDisabled();
+ await PageObjects.observability.expectCreateCaseButtonMissing();
});
it(`does not allow a case to be edited`, async () => {
@@ -162,7 +162,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
shouldUseHashForSubUrl: false,
}
);
- await PageObjects.observability.expectAddCommentButtonDisabled();
+ await PageObjects.observability.expectAddCommentButtonMissing();
});
});
diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts
index 0e0203046fd16e..df92c1c398d93d 100644
--- a/x-pack/test/functional/page_objects/canvas_page.ts
+++ b/x-pack/test/functional/page_objects/canvas_page.ts
@@ -39,7 +39,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo
* to load the workpad. Resolves once the workpad is in the DOM
*/
async loadFirstWorkpad(workpadName: string) {
- const elem = await testSubjects.find('canvasWorkpadLoaderWorkpad');
+ const elem = await testSubjects.find('canvasWorkpadTableWorkpad');
const text = await elem.getVisibleText();
expect(text).to.be(workpadName);
await elem.click();
diff --git a/x-pack/test/functional/page_objects/observability_page.ts b/x-pack/test/functional/page_objects/observability_page.ts
index 95016c31d10541..d9e413d473adf3 100644
--- a/x-pack/test/functional/page_objects/observability_page.ts
+++ b/x-pack/test/functional/page_objects/observability_page.ts
@@ -20,14 +20,12 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro
expect(disabledAttr).to.be(null);
},
- async expectCreateCaseButtonDisabled() {
- const button = await testSubjects.find('createNewCaseBtn', 20000);
- const disabledAttr = await button.getAttribute('disabled');
- expect(disabledAttr).to.be('true');
+ async expectCreateCaseButtonMissing() {
+ await testSubjects.missingOrFail('createNewCaseBtn');
},
- async expectReadOnlyCallout() {
- await testSubjects.existOrFail('case-callout-e41900b01c9ef0fa81dd6ff326083fb3');
+ async expectReadOnlyGlassesBadge() {
+ await testSubjects.existOrFail('headerBadge');
},
async expectNoReadOnlyCallout() {
@@ -44,10 +42,8 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro
expect(disabledAttr).to.be(null);
},
- async expectAddCommentButtonDisabled() {
- const button = await testSubjects.find('submit-comment', 20000);
- const disabledAttr = await button.getAttribute('disabled');
- expect(disabledAttr).to.be('true');
+ async expectAddCommentButtonMissing() {
+ await testSubjects.missingOrFail('submit-comment');
},
async expectForbidden() {
diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts
index b712c2882ee0f9..eb0c161049cf05 100644
--- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts
+++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts
@@ -154,12 +154,14 @@ export const expectResponses = {
// bulk request error
expect(object.type).to.eql(type);
expect(object.id).to.eql(id);
- expect(object.error).to.eql(error.output.payload);
+ expect(object.error.error).to.eql(error.output.payload.error);
+ expect(object.error.statusCode).to.eql(error.output.payload.statusCode);
+ // ignore the error.message, because it can vary for decorated errors
} else {
// non-bulk request error
expect(object.error).to.eql(error.output.payload.error);
expect(object.statusCode).to.eql(error.output.payload.statusCode);
- // ignore the error.message, because it can vary for decorated non-bulk errors (e.g., conflict)
+ // ignore the error.message, because it can vary for decorated errors
}
} else {
// fall back to default behavior of testing the success outcome
diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
index 5860ec1f193b27..06758da1ebad27 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
@@ -41,13 +41,25 @@ const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID];
const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' });
const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' });
-const NEW_EACH_SPACE_OBJ = Object.freeze({
+const INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE = Object.freeze({
+ type: 'isolatedtype',
+ id: 'new-other-space-id',
+ expectedNamespaces: ['other-space'], // expected namespaces of resulting object
+ initialNamespaces: ['other-space'], // args passed to the bulkCreate method
+});
+const INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE = Object.freeze({
+ type: 'sharecapabletype',
+ id: 'new-other-space-id',
+ expectedNamespaces: ['other-space'], // expected namespaces of resulting object
+ initialNamespaces: ['other-space'], // args passed to the bulkCreate method
+});
+const INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE = Object.freeze({
type: 'sharedtype',
id: 'new-each-space-id',
expectedNamespaces: EACH_SPACE, // expected namespaces of resulting object
initialNamespaces: EACH_SPACE, // args passed to the bulkCreate method
});
-const NEW_ALL_SPACES_OBJ = Object.freeze({
+const INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES = Object.freeze({
type: 'sharedtype',
id: 'new-all-spaces-id',
expectedNamespaces: [ALL_SPACES_ID], // expected namespaces of resulting object
@@ -58,8 +70,10 @@ export const TEST_CASES: Record = Object.freeze({
...CASES,
NEW_SINGLE_NAMESPACE_OBJ,
NEW_MULTI_NAMESPACE_OBJ,
- NEW_EACH_SPACE_OBJ,
- NEW_ALL_SPACES_OBJ,
+ INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
NEW_NAMESPACE_AGNOSTIC_OBJ,
});
diff --git a/x-pack/test/saved_object_api_integration/common/suites/create.ts b/x-pack/test/saved_object_api_integration/common/suites/create.ts
index ff2bfdefb4c087..298e1a98071754 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/create.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/create.ts
@@ -41,13 +41,25 @@ const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID];
// we could create six separate test cases to test every permutation, but there's no real value in doing so
const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: '' });
const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' });
-const NEW_EACH_SPACE_OBJ = Object.freeze({
+const INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE = Object.freeze({
+ type: 'isolatedtype',
+ id: 'new-other-space-id',
+ expectedNamespaces: ['other-space'], // expected namespaces of resulting object
+ initialNamespaces: ['other-space'], // args passed to the bulkCreate method
+});
+const INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE = Object.freeze({
+ type: 'sharecapabletype',
+ id: 'new-other-space-id',
+ expectedNamespaces: ['other-space'], // expected namespaces of resulting object
+ initialNamespaces: ['other-space'], // args passed to the bulkCreate method
+});
+const INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE = Object.freeze({
type: 'sharedtype',
id: 'new-each-space-id',
expectedNamespaces: EACH_SPACE, // expected namespaces of resulting object
initialNamespaces: EACH_SPACE, // args passed to the bulkCreate method
});
-const NEW_ALL_SPACES_OBJ = Object.freeze({
+const INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES = Object.freeze({
type: 'sharedtype',
id: 'new-all-spaces-id',
expectedNamespaces: [ALL_SPACES_ID], // expected namespaces of resulting object
@@ -58,8 +70,10 @@ export const TEST_CASES: Record = Object.freeze({
...CASES,
NEW_SINGLE_NAMESPACE_OBJ,
NEW_MULTI_NAMESPACE_OBJ,
- NEW_EACH_SPACE_OBJ,
- NEW_ALL_SPACES_OBJ,
+ INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
NEW_NAMESPACE_AGNOSTIC_OBJ,
});
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
index 1fa24c6d6e2d6f..e048a4abc8ccc5 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -75,7 +75,22 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
];
- const crossNamespace = [CASES.NEW_EACH_SPACE_OBJ, CASES.NEW_ALL_SPACES_OBJ];
+ const crossNamespace = [
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
+ ];
const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }];
const allTypes = normalTypes.concat(hiddenType);
return { normalTypes, crossNamespace, hiddenType, allTypes };
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts
index 3553ae0e5b5387..8215c991a9287d 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -62,7 +62,22 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
];
- const crossNamespace = [CASES.NEW_EACH_SPACE_OBJ, CASES.NEW_ALL_SPACES_OBJ];
+ const crossNamespace = [
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
+ ];
const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }];
const allTypes = normalTypes.concat(crossNamespace, hiddenType);
return { normalTypes, crossNamespace, hiddenType, allTypes };
diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts
index 7487466f4b38c0..f9423d77c5bb56 100644
--- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -39,8 +39,20 @@ const createTestCases = (overwrite: boolean) => {
{ ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces },
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
- CASES.NEW_EACH_SPACE_OBJ,
- CASES.NEW_ALL_SPACES_OBJ,
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
];
const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }];
const allTypes = normalTypes.concat(hiddenType);
diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts
index 7eda7f52834480..67195637f0c0ac 100644
--- a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts
+++ b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -38,8 +38,20 @@ const createTestCases = (overwrite: boolean) => {
{ ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces },
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
- CASES.NEW_EACH_SPACE_OBJ,
- CASES.NEW_ALL_SPACES_OBJ,
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
];
const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }];
const allTypes = normalTypes.concat(hiddenType);
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
index 5812aaf43060d3..c448d73ce7bf83 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { bulkCreateTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/bulk_create';
@@ -70,8 +70,20 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
{ ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces },
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
- CASES.NEW_EACH_SPACE_OBJ,
- CASES.NEW_ALL_SPACES_OBJ,
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
];
};
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts
index 4c91781b6ab2c0..7c8726896c18a1 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { createTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/create';
@@ -57,8 +57,20 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
{ ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces },
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
- CASES.NEW_EACH_SPACE_OBJ,
- CASES.NEW_ALL_SPACES_OBJ,
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
];
};
diff --git a/yarn.lock b/yarn.lock
index 153309ad56f191..953e7907590e77 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2788,7 +2788,7 @@
version "0.0.0"
uid ""
-"@kbn/ui-framework@link:packages/kbn-ui-framework":
+"@kbn/ui-framework@link:bazel-bin/packages/kbn-ui-framework":
version "0.0.0"
uid ""