diff --git a/docs/api/task-manager/health.asciidoc b/docs/api/task-manager/health.asciidoc index 22006725da00c..7418d44bbfd33 100644 --- a/docs/api/task-manager/health.asciidoc +++ b/docs/api/task-manager/health.asciidoc @@ -6,17 +6,20 @@ Retrieve the health status of the {kib} Task Manager. +[float] [[task-manager-api-health-request]] ==== Request `GET :/api/task_manager/_health` +[float] [[task-manager-api-health-codes]] ==== Response code `200`:: Indicates a successful call. +[float] [[task-manager-api-health-example]] ==== Example diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 217645b903818..0ee4c09192896 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -103,6 +103,7 @@ yarn kbn watch-bazel - @kbn/securitysolution-utils - @kbn/server-http-tools - @kbn/server-route-repository +- @kbn/spec-to-console - @kbn/std - @kbn/storybook - @kbn/telemetry-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 b10ad949c4944..63d791db452d0 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 @@ -8,6 +8,7 @@ ```typescript readonly links: { + readonly settings: string; readonly canvas: { 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 c020f57faa882..947eece498130 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 sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly 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/user/dashboard/images/tsvb_group_by_multiple_fields.png b/docs/user/dashboard/images/tsvb_group_by_multiple_fields.png new file mode 100644 index 0000000000000..3f23189f77254 Binary files /dev/null and b/docs/user/dashboard/images/tsvb_group_by_multiple_fields.png differ diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index b69df7c7d26d6..93ee3627bd8a0 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -166,3 +166,15 @@ For other types of month over month calculations, use <> o Calculating the duration between the start and end of an event is unsupported in *TSVB* because *TSVB* requires correlation between different time periods. *TSVB* requires that the duration is pre-calculated. + +[float] +===== How do I group on multiple fields? + +To group with multiple fields, create runtime fields in the index pattern you are visualizing. + +. Create a runtime field. Refer to <> for more information. ++ +[role="screenshot"] +image::images/tsvb_group_by_multiple_fields.png[Group by multiple fields] + +. Create a new TSVB visualization and group by this field. \ No newline at end of file diff --git a/package.json b/package.json index ceb178d068519..b589153d2af90 100644 --- a/package.json +++ b/package.json @@ -470,6 +470,7 @@ "@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator", "@kbn/plugin-helpers": "link:bazel-bin/packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", + "@kbn/spec-to-console": "link:bazel-bin/packages/kbn-spec-to-console", "@kbn/storybook": "link:bazel-bin/packages/kbn-storybook", "@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools", "@kbn/test": "link:packages/kbn-test", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 1094a2def3e70..225a41a5fd8b6 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -48,6 +48,7 @@ filegroup( "//packages/kbn-securitysolution-hook-utils:build", "//packages/kbn-server-http-tools:build", "//packages/kbn-server-route-repository:build", + "//packages/kbn-spec-to-console:build", "//packages/kbn-std:build", "//packages/kbn-storybook:build", "//packages/kbn-telemetry-tools:build", diff --git a/packages/kbn-spec-to-console/BUILD.bazel b/packages/kbn-spec-to-console/BUILD.bazel new file mode 100644 index 0000000000000..8a083be9fce91 --- /dev/null +++ b/packages/kbn-spec-to-console/BUILD.bazel @@ -0,0 +1,55 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-spec-to-console" +PKG_REQUIRE_NAME = "@kbn/spec-to-console" + +SOURCE_FILES = glob( + [ + "bin/**/*", + "lib/**/*", + "index.js" + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +DEPS = [] + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES + [ + ":srcs", + ], + deps = DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-spec-to-console/test/fixtures/cluster_health_autocomplete.json b/packages/kbn-spec-to-console/lib/__fixtures__/cluster_health_autocomplete.json similarity index 100% rename from packages/kbn-spec-to-console/test/fixtures/cluster_health_autocomplete.json rename to packages/kbn-spec-to-console/lib/__fixtures__/cluster_health_autocomplete.json diff --git a/packages/kbn-spec-to-console/test/fixtures/cluster_health_spec.json b/packages/kbn-spec-to-console/lib/__fixtures__/cluster_health_spec.json similarity index 100% rename from packages/kbn-spec-to-console/test/fixtures/cluster_health_spec.json rename to packages/kbn-spec-to-console/lib/__fixtures__/cluster_health_spec.json diff --git a/packages/kbn-spec-to-console/lib/convert.test.js b/packages/kbn-spec-to-console/lib/convert.test.js index 6d6b6ba364d38..14cb2dd7b6c04 100644 --- a/packages/kbn-spec-to-console/lib/convert.test.js +++ b/packages/kbn-spec-to-console/lib/convert.test.js @@ -8,8 +8,8 @@ const convert = require('./convert'); -const clusterHealthSpec = require('../test/fixtures/cluster_health_spec'); -const clusterHealthAutocomplete = require('../test/fixtures/cluster_health_autocomplete'); +const clusterHealthSpec = require('./__fixtures__/cluster_health_spec'); +const clusterHealthAutocomplete = require('./__fixtures__/cluster_health_autocomplete'); test('convert', () => { expect(convert(clusterHealthSpec)).toEqual(clusterHealthAutocomplete); diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index c6cf8cf9ec46d..b4e488db7f4d9 100644 --- a/packages/kbn-spec-to-console/package.json +++ b/packages/kbn-spec-to-console/package.json @@ -1,11 +1,12 @@ { - "name": "spec-to-console", - "version": "0.0.0", + "name": "@kbn/spec-to-console", + "version": "1.0.0", "description": "ES REST spec -> Console autocomplete", "main": "index.js", "directories": { "lib": "lib" }, + "private": true, "scripts": { "format": "../../node_modules/.bin/prettier **/*.js --write" }, diff --git a/rfcs/images/0019_lifecycle_preboot.png b/rfcs/images/0019_lifecycle_preboot.png new file mode 100644 index 0000000000000..73eeb901eb33f Binary files /dev/null and b/rfcs/images/0019_lifecycle_preboot.png differ diff --git a/rfcs/text/0019_lifecycle_preboot.md b/rfcs/text/0019_lifecycle_preboot.md new file mode 100644 index 0000000000000..d1448ad4ce15a --- /dev/null +++ b/rfcs/text/0019_lifecycle_preboot.md @@ -0,0 +1,261 @@ +- Start Date: 2020-06-04 +- RFC PR: (leave this empty) +- Kibana Issue: https://github.com/elastic/kibana/issues/89287 + +--- +- [1. Summary](#1-summary) +- [2. Motivation](#2-motivation) +- [3. Detailed design](#3-detailed-design) + - [3.1 Core client-side changes](#31-core-client-side-changes) + - [3.2 Core server-side changes](#32-core-server-side-changes) + - [3.2.1 Plugins service](#321-plugins-service) + - [3.2.2 HTTP service](#322-http-service) + - [3.2.3 Elasticsearch service](#323-elasticsearch-service) + - [3.2.4 UI Settings service](#324-ui-settings-service) + - [3.2.5 Rendering service](#325-rendering-service) + - [3.2.6 I18n service](#326-i18n-service) + - [3.2.7 Environment service](#327-environment-service) + - [3.2.8 Core app service](#328-core-app-service) + - [3.2.9 Preboot service](#329-preboot-service) + - [3.2.10 Bootstrap](#3210-bootstrap) +- [4. Drawbacks](#4-drawbacks) +- [5. Alternatives](#5-alternatives) +- [6. Adoption strategy](#6-adoption-strategy) +- [7. How we teach this](#7-how-we-teach-this) +- [8. Unresolved questions](#8-unresolved-questions) + - [8.1 Lifecycle stage name](#81-lifecycle-stage-name) + - [8.2 Development mode and basepath proxy](#82-development-mode-and-basepath-proxy) +- [9. Resolved questions](#9-resolved-questions) + - [9.1 Core client-side changes](#91-core-client-side-changes) + +# 1. Summary + +The `preboot` (see [unresolved question 1](#81-lifecycle-stage-name)) is the Kibana initial lifecycle stage at which it only initializes a bare minimum of the core services and a limited set of special-purpose plugins. It's assumed that Kibana can change and reload its own configuration at this stage and may require administrator involvement before it can proceed to the `setup` and `start` stages. + +# 2. Motivation + +The `preboot` lifecycle stage is a prerequisite for the Kibana interactive setup mode. This is the mode Kibana enters to on the first launch if it detects that user hasn't explicitly configured their own connection to Elasticsearch. In this mode, Kibana will present an interface to the user that would allow them to provide Elasticsearch connection information and potentially any other configuration information. Once the information is verified, Kibana will write it to the disk and allow the rest of Kibana to start. + +The interactive setup mode will be provided through a dedicated `userSetup` plugin that will be initialized at the `preboot` stage. + +# 3. Detailed design + +The central part of the `preboot` stage is a dedicated HTTP server instance formerly known as `Not Ready` server. Kibana starts this server at the `preboot` stage and shuts it down as soon as the main HTTP server is ready to start, as illustrated at the following diagram: + +![Preboot plugins lifetime](../images/0019_lifecycle_preboot.png) + +Currently, preboot HTTP server only exposes a status endpoint and renders a static `Kibana server is not ready yet` string whenever users try to access Kibana before it's completely initialized. The changes proposed in this RFC should allow special-purpose plugins to define custom HTTP endpoints, and serve interactive client-side applications on this server, and hence make Kibana interactive setup mode possible. + +## 3.1 Core client-side changes + +The RFC aims to limit the changes to only those that are absolutely required and doesn't assume any modifications in the client-side part of the Kibana Core at the moment. This may introduce a certain level of inconsistency in the client-side codebase, but we consider it insignificant. See [resolved question 1](#91-core-client-side-changes) for more details. + +## 3.2 Core server-side changes + +We'll update only several Core server-side services to support the new `preboot` lifecycle stage and preboot plugins. + +Once none of the `preboot` plugins holds the `setup` anymore, Kibana might need to reload the configuration before it can finally proceed to `setup`. This doesn't require any special care from the existing plugin developers since Kibana would instantiate plugins only after it reloads the config. We'll also make sure that neither of the Core services relies on the stale configuration it may have acquired during the `preboot` stage. + +### 3.2.1 Plugins service + +First of all, we'll introduce a new type of special-purpose plugins: preboot plugins, in contrast to standard plugins. Kibana will initialize preboot plugins at the `preboot` stage, before even instantiating standard plugins. + +Preboot plugins have only `setup` and `stop` methods, and can only depend on other preboot plugins. Standard plugins cannot depend on the preboot plugins since Kibana will stop them before starting the standard plugins: + +```ts +export interface PrebootPlugin { + setup(core: CorePrebootSetup, plugins: TPluginsSetup): TSetup; + stop?(): void; +} +``` + +To differentiate preboot and standard plugins we'll introduce a new _optional_ `type` property in the plugin manifest. The property can have only two possible values: `preboot` for `preboot` plugins and `standard` for the standard ones. If `type` is omitted, the `standard` value will be assumed. + +```json5 +// NOTE(azasypkin): all other existing properties have been omitted for brevity. +{ + "type": "preboot", // 'preboot' | 'standard' | undefined +} +``` + +The Plugins service will split plugins into two separate groups during discovery to use them separately at the `preboot`, `setup`, and `start` stages. The Core contract that preboot plugins will receive during their `setup` will be different from the one standard plugins receive, and will only include the functionality that is currently required for the interactive setup mode. We'll discuss this functionality in details in the following sections: + +```ts +export interface CorePrebootSetup { + elasticsearch: ElasticsearchServicePrebootSetup; + http: HttpServicePrebootSetup; + preboot: PrebootServiceSetup; +} +``` + +### 3.2.2 HTTP service + +We'll change HTTP service to initialize and start preboot HTTP server (formerly known as `Not Ready` server) in the new `preboot` method instead of `setup`. The returned `InternalHttpServicePrebootSetup` contract will presumably be very similar to the existing `InternalHttpServiceSetup` contract, but will only include APIs we currently need to support interactive setup mode: + +```ts +// NOTE(azasypkin): some existing properties have been omitted for brevity. +export interface InternalHttpServicePrebootSetup + extends Pick { + server: HttpServerSetup['server']; + externalUrl: ExternalUrlConfig; + registerRoutes(path: string, callback: (router: IRouter) => void): void; +} +``` + +The only part of this contract that will be available to the preboot plugins via `CorePrebootSetup` is the API to register HTTP routes on the already running preboot HTTP server: + +```ts +export interface HttpServicePrebootSetup { + registerRoutes(path: string, callback: (router: IRouter) => void): void; +} +``` + +The Core HTTP context available to handlers of the routes registered on the preboot HTTP server will only expose the `uiSettings` service. As explained in the [UI Settings service section](#324-ui-settings-service), this service will only give access to the **default Core** UI settings and their overrides set through Kibana configuration, if any. +```ts +// NOTE(azasypkin): the fact that the client is lazily initialized has been omitted for brevity. +export interface PrebootCoreRouteHandlerContext { + readonly uiSettings: { client: IUiSettingsClient }; +} +``` + +The authentication and authorization components are not available at the `preboot` stage, and hence all preboot HTTP server routes can be freely accessed by anyone with access to the network Kibana is exposed to. + +Just as today, Kibana will shut the preboot HTTP server down as soon as it's ready to start the main HTTP server. + +### 3.2.3 Elasticsearch service + +As mentioned in the [Motivation section](#2-motivation), the main goal of the interactive setup mode is to give the user a hassle-free way to configure Kibana connection to an Elasticsearch cluster. That means that users might provide certain connection information, and Kibana preboot plugins should be able to construct a new Elasticsearch client using this information to verify it and potentially call Elasticsearch APIs. + +To support this use case we'll add a new `preboot` method to the Elasticsearch service that will return the following contract, and make it available to the preboot plugins via `CorePrebootSetup`: + +```ts +export interface ElasticsearchServicePrebootSetup { + readonly createClient: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; +} +``` + +The Elasticsearch clients created with `createClient` rely on the default Kibana Elasticsearch configuration and any configuration overrides specified by the consumer. + +__NOTE:__ We may need to expose a full or portion of Elasticsearch config to the preboot plugins for them to check if the user has already configured Elasticsearch connection. There are other ways to check that without direct access to the configuration though. + +### 3.2.4 UI Settings service + +We'll introduce a new `preboot` method in the UI Settings service that will produce a UI Settings client instance. Since during the `preboot` stage Kibana can access neither user information nor Saved Objects, this client will only give access to the **default Core** UI settings and their overrides set through Kibana configuration, if any: + +```ts +export interface InternalUiSettingsServicePrebootSetup { + defaultsClient(): IUiSettingsClient; +} +``` + +UI Settings service isn't strictly necessary during the `preboot` stage, but many Kibana Core components rely on it explicitly and implicitly, which justifies this simple change. + +### 3.2.5 Rendering service + +We'll introduce a new `preboot` method in the Rendering service that will register Kibana main UI bootstrap template route on the preboot HTTP server as it does for the main HTTP server today. The main difference is that bootstrap UI will only reference bundles of the preboot plugins and will rely on the default UI settings. + +### 3.2.6 I18n service + +We'll introduce a new `preboot` method in the I18n service to only include translations for the Core itself and preboot plugins in the translations bundle loaded with the preboot UI bootstrap template. This would potentially allow us to switch locale during interactive setup mode if there is such a need in the future. + +### 3.2.7 Environment service + +There are no changes required in the Environment service itself, but we'll expose one additional property from its `setup` contract to the plugins: the paths to the known configuration files. The interactive setup mode should be able to figure out to which configuration file Kibana should save any changes users might need to make. + +### 3.2.8 Core app service + +We'll introduce a new `preboot` method in the Core app service to register routes on the preboot HTTP server necessary for the rendering of the Kibana preboot applications. Most of the routes will be the same as for the main HTTP server, but there are three notable exceptions: + +1. JS bundles routes will only include those exposed by the preboot plugins + +2. Default route for the preboot HTTP server will be hardcoded to the root path (`/`) since we cannot rely on the default value of the `defaultRoute` UI setting (`/app/home`) + +3. Main application route (`/app/{id}/{any*}`) will be replaced with the catch-all route (`/{path*}`). The reason is that if the user tries to access Kibana with a legit standard application URL (e.g. `/app/discover/?parameters`) while Kibana is still at the `preboot` stage, they will end up with `Application is not found` error. Instead, with the catch-all route, Kibana will capture the original URL in the `next` query string parameter and redirect the user to the root (e.g. `/?next=%2Fapp%2Fdiscover%2F%3Fparameters`). This will allow us to automatically redirect the user back to the original URL as soon as Kibana is ready. The main drawback and limitation of this approach are that there can be only one root-level preboot application. We can lift this limitation in the future if we have to though, for example, to support post-preboot Saved Objects migration UI or something similar. + +Serving a proper Kibana application on the root route of the preboot HTTP server implies that we'll also have a chance to replace the static `Kibana server is not ready yet` string with a more helpful and user-friendly application. Such application may potentially display a certain set of Kibana status information. + +### 3.2.9 Preboot service + +To support interactive applications at the `preboot` stage we should allow preboot plugins to pause Kibana startup sequence. This functionality will be exposed by the new Preboot service, and will be available to the preboot plugins via `CorePrebootSetup`. Preboot plugins will be able to provide a promise to hold `setup` and/or `start` for as long as needed, and also let Kibana know if it has to reload configuration before it enters the `setup` stage. + +```ts +export interface PrebootServiceSetup { + readonly isSetupOnHold: () => boolean; + readonly holdSetupUntilResolved: ( + reason: string, + promise: Promise<{ shouldReloadConfig: boolean } | void> + ) => void; + readonly isStartOnHold: () => boolean; + readonly holdStartUntilResolved: ( + reason: string, + promise: Promise + ) => void +} +``` + +Preboot service will provide a pair of helper `isSetupOnHold` and `isStartOnHold` methods that would allow consumers to check if `setup` or `start` are on hold before they are blocked on waiting. + +Internal Preboot service contract will also expose `waitUntilCanSetup` and `waitUntilCanStart` methods that bootstrap process can use to know when it can proceed to `setup` and `start` stages. If any of these methods returns a `Promise` that is rejected, Kibana will shut down. + +```ts +// NOTE(azasypkin): some existing properties have been omitted for brevity. +export interface InternalPrebootServiceSetup { + readonly waitUntilCanSetup: () => Promise<{ shouldReloadConfig: boolean } | void>; + readonly waitUntilCanStart: () => Promise; +} +``` + +### 3.2.10 Bootstrap + +We'll update Kibana bootstrap sequence to include `preboot` stage and to conditionally reload configuration before proceeding to `setup` and `start` stages: + +```ts +// NOTE(azasypkin): some functionality and checks have been omitted for brevity. +const { preboot } = await root.preboot(); + +const { shouldReloadConfig } = await preboot.waitUntilCanSetup(); +if (shouldReloadConfig) { + await reloadConfiguration('pre-boot request'); +} +await root.setup(); + +await preboot.waitUntilCanStart(); +await root.start(); +``` + +It's not yet clear if we need to adjust the base path proxy to account for this new lifecycle stage (see [unresolved question 2](#82-development-mode-and-basepath-proxy)). + +# 4. Drawbacks + +The main drawback is that proposed changes affect quite a few Kibana Core services that may impose a risk of breaking something in the critical parts of Kibana. + +# 5. Alternatives + +The most viable alternative to support interactive setup mode for Kibana was a standalone application that would be completely separated from Kibana. We ruled out this option since we won't be able to leverage existing and battle-tested Core services, UI components, and development tools. This would make the long-term maintenance burden unreasonably high. + +# 6. Adoption strategy + +The new `preboot` stage doesn't need an adoption strategy since it's intended for internal platform use only. + +# 7. How we teach this + +The new `preboot` stage shouldn't need much knowledge sharing since it's intended for internal platform use only and doesn't affect the standard plugins. All new services, methods, and contracts will be sufficiently documented in the code. + +# 8. Unresolved questions + +## 8.1 Lifecycle stage name + +Is `preboot` the right name for this new lifecycle stage? Do we have a better alternative? + +## 8.2 Development mode and basepath proxy + +Currently, the base path proxy blocks any requests to Kibana until it receives `SERVER_LISTENING` message. Kibana's main process sends this message only after `start`, but we should change that to support interactive preboot applications. It's not yet clear how big the impact of this change will be. + +# 9. Resolved questions + +## 9.1 Core client-side changes + +The server-side part of the `preboot` plugins will follow a new `PrebootPlugin` interface that doesn't have a `start` method, but the client-side part will stay the same as for standard plugins. This significantly simplifies implementation and doesn't introduce any known technical issues, but, unfortunately, brings some inconsistency to the codebase. We agreed that it's tolerable assuming we define a dedicated client-side `PrebootPlugin` interface that would hide from `CoreStart` all services that are unavailable to the preboot plugins (e.g., Saved Objects service). \ No newline at end of file diff --git a/scripts/spec_to_console.js b/scripts/spec_to_console.js index cbb152f55f8fb..37e246323a11f 100644 --- a/scripts/spec_to_console.js +++ b/scripts/spec_to_console.js @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -require('../packages/kbn-spec-to-console/bin/spec_to_console'); +require('@kbn/spec-to-console/bin/spec_to_console'); diff --git a/src/core/public/core_app/core_app.ts b/src/core/public/core_app/core_app.ts index aa0223dbe08a7..00532b9150aef 100644 --- a/src/core/public/core_app/core_app.ts +++ b/src/core/public/core_app/core_app.ts @@ -18,8 +18,13 @@ import type { CoreContext } from '../core_system'; import type { NotificationsSetup, NotificationsStart } from '../notifications'; import type { IUiSettingsClient } from '../ui_settings'; import type { InjectedMetadataSetup } from '../injected_metadata'; -import { renderApp as renderErrorApp, setupUrlOverflowDetection } from './errors'; +import { + renderApp as renderErrorApp, + setupPublicBaseUrlConfigWarning, + setupUrlOverflowDetection, +} from './errors'; import { renderApp as renderStatusApp } from './status'; +import { DocLinksStart } from '../doc_links'; interface SetupDeps { application: InternalApplicationSetup; @@ -30,6 +35,7 @@ interface SetupDeps { interface StartDeps { application: InternalApplicationStart; + docLinks: DocLinksStart; http: HttpStart; notifications: NotificationsStart; uiSettings: IUiSettingsClient; @@ -40,7 +46,7 @@ export class CoreApp { constructor(private readonly coreContext: CoreContext) {} - public setup({ http, application, injectedMetadata, notifications }: SetupDeps) { + public setup({ application, http, injectedMetadata, notifications }: SetupDeps) { application.register(this.coreContext.coreId, { id: 'error', title: 'App Error', @@ -68,7 +74,7 @@ export class CoreApp { }); } - public start({ application, http, notifications, uiSettings }: StartDeps) { + public start({ application, docLinks, http, notifications, uiSettings }: StartDeps) { if (!application.history) { return; } @@ -79,6 +85,8 @@ export class CoreApp { toasts: notifications.toasts, uiSettings, }); + + setupPublicBaseUrlConfigWarning({ docLinks, http, notifications }); } public stop() { diff --git a/src/core/public/core_app/errors/index.ts b/src/core/public/core_app/errors/index.ts index 02666103de349..e991fa455ab31 100644 --- a/src/core/public/core_app/errors/index.ts +++ b/src/core/public/core_app/errors/index.ts @@ -8,3 +8,4 @@ export { renderApp } from './error_application'; export { setupUrlOverflowDetection, URL_MAX_LENGTH } from './url_overflow'; +export { setupPublicBaseUrlConfigWarning } from './public_base_url'; diff --git a/src/core/public/core_app/errors/public_base_url.test.tsx b/src/core/public/core_app/errors/public_base_url.test.tsx new file mode 100644 index 0000000000000..d1fb5a5093f15 --- /dev/null +++ b/src/core/public/core_app/errors/public_base_url.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { docLinksServiceMock } from '../../doc_links/doc_links_service.mock'; +import { httpServiceMock } from '../../http/http_service.mock'; +import { notificationServiceMock } from '../../notifications/notifications_service.mock'; + +import { setupPublicBaseUrlConfigWarning } from './public_base_url'; + +describe('publicBaseUrl warning', () => { + const docLinks = docLinksServiceMock.createStartContract(); + const notifications = notificationServiceMock.createStartContract(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('does not show any toast on localhost', () => { + const http = httpServiceMock.createStartContract(); + + setupPublicBaseUrlConfigWarning({ + docLinks, + notifications, + http, + location: { + hostname: 'localhost', + } as Location, + }); + + expect(notifications.toasts.addWarning).not.toHaveBeenCalled(); + }); + + it('does not show any toast on 127.0.0.1', () => { + const http = httpServiceMock.createStartContract(); + + setupPublicBaseUrlConfigWarning({ + docLinks, + notifications, + http, + location: { + hostname: '127.0.0.1', + } as Location, + }); + + expect(notifications.toasts.addWarning).not.toHaveBeenCalled(); + }); + + it('does not show toast if configured correctly', () => { + const http = httpServiceMock.createStartContract({ publicBaseUrl: 'http://myhost.com' }); + + setupPublicBaseUrlConfigWarning({ + docLinks, + notifications, + http, + location: { + hostname: 'myhost.com', + toString() { + return 'http://myhost.com/'; + }, + } as Location, + }); + + expect(notifications.toasts.addWarning).not.toHaveBeenCalled(); + }); + + describe('config missing toast', () => { + it('adds toast if publicBaseUrl is missing', () => { + const http = httpServiceMock.createStartContract({ publicBaseUrl: undefined }); + + setupPublicBaseUrlConfigWarning({ + docLinks, + notifications, + http, + location: { + hostname: 'myhost.com', + toString() { + return 'http://myhost.com/'; + }, + } as Location, + }); + + expect(notifications.toasts.addWarning).toHaveBeenCalledWith({ + title: 'Configuration missing', + text: expect.any(Function), + }); + }); + + it('does not add toast if storage key set', () => { + const http = httpServiceMock.createStartContract({ publicBaseUrl: undefined }); + + setupPublicBaseUrlConfigWarning({ + docLinks, + notifications, + http, + location: { + hostname: 'myhost.com', + toString() { + return 'http://myhost.com/'; + }, + } as Location, + storage: { + getItem: (id: string) => 'true', + } as Storage, + }); + + expect(notifications.toasts.addWarning).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/core/public/core_app/errors/public_base_url.tsx b/src/core/public/core_app/errors/public_base_url.tsx new file mode 100644 index 0000000000000..263367a4cb09a --- /dev/null +++ b/src/core/public/core_app/errors/public_base_url.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import type { HttpStart, NotificationsStart } from '../..'; +import type { DocLinksStart } from '../../doc_links'; +import { mountReactNode } from '../../utils'; + +/** Only exported for tests */ +export const MISSING_CONFIG_STORAGE_KEY = `core.warnings.publicBaseUrlMissingDismissed`; + +interface Deps { + docLinks: DocLinksStart; + http: HttpStart; + notifications: NotificationsStart; + // Exposed for easier testing + storage?: Storage; + location?: Location; +} + +export const setupPublicBaseUrlConfigWarning = ({ + docLinks, + http, + notifications, + storage = window.localStorage, + location = window.location, +}: Deps) => { + if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { + return; + } + + const missingWarningSeen = storage.getItem(MISSING_CONFIG_STORAGE_KEY) === 'true'; + if (missingWarningSeen || http.basePath.publicBaseUrl) { + return; + } + + const toast = notifications.toasts.addWarning({ + title: i18n.translate('core.ui.publicBaseUrlWarning.configMissingTitle', { + defaultMessage: 'Configuration missing', + }), + text: mountReactNode( + <> +

+ server.publicBaseUrl, + }} + />{' '} + + + +

+ + + + { + notifications.toasts.remove(toast); + storage.setItem(MISSING_CONFIG_STORAGE_KEY, 'true'); + }} + id="mute" + > + + + + + + ), + }); +}; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 9a28bf45df927..e5dcd8f817a0a 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -202,7 +202,7 @@ export class CoreSystem { }); const deprecations = this.deprecations.start({ http }); - this.coreApp.start({ application, http, notifications, uiSettings }); + this.coreApp.start({ application, docLinks, http, notifications, uiSettings }); const core: InternalCoreStart = { application, diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 502b22a6f8e89..43c21b37ee298 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -29,6 +29,7 @@ export class DocLinksService { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL, links: { + settings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/settings.html`, canvas: { guide: `${KIBANA_DOCS}canvas.html`, }, @@ -426,6 +427,7 @@ export interface DocLinksStart { readonly DOC_LINK_VERSION: string; readonly ELASTIC_WEBSITE_URL: string; readonly links: { + readonly settings: string; readonly canvas: { readonly guide: string; }; diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 61f501c844f30..fff99d84a76a6 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -18,7 +18,10 @@ export type HttpSetupMock = jest.Mocked & { anonymousPaths: jest.Mocked; }; -const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ +const createServiceMock = ({ + basePath = '', + publicBaseUrl, +}: { basePath?: string; publicBaseUrl?: string } = {}): HttpSetupMock => ({ fetch: jest.fn(), get: jest.fn(), head: jest.fn(), @@ -27,7 +30,7 @@ const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ patch: jest.fn(), delete: jest.fn(), options: jest.fn(), - basePath: new BasePath(basePath), + basePath: new BasePath(basePath, undefined, publicBaseUrl), anonymousPaths: { register: jest.fn(), isAnonymous: jest.fn(), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index ca95b253f9cdb..32897f10425d6 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -487,6 +487,7 @@ export interface DocLinksStart { readonly ELASTIC_WEBSITE_URL: string; // (undocumented) readonly links: { + readonly settings: string; readonly canvas: { readonly guide: string; }; diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.ts b/src/plugins/data/common/es_query/filters/phrases_filter.ts index 8a79472154493..2694461fc1930 100644 --- a/src/plugins/data/common/es_query/filters/phrases_filter.ts +++ b/src/plugins/data/common/es_query/filters/phrases_filter.ts @@ -41,11 +41,6 @@ export const buildPhrasesFilter = ( const type = FILTERS.PHRASES; const key = field.name; - const format = (f: IFieldType, value: any) => - f && f.format && f.format.convert ? f.format.convert(value) : value; - - const value = params.map((v: any) => format(field, v)).join(', '); - let should; if (field.scripted) { should = params.map((v: any) => ({ @@ -60,7 +55,7 @@ export const buildPhrasesFilter = ( } return { - meta: { index, type, key, value, params }, + meta: { index, type, key, params }, query: { bool: { should, diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts index 7bc7a8cff7487..9f1d9a5d08926 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.ts @@ -84,10 +84,7 @@ export const getRangeFilterField = (filter: RangeFilter) => { }; const formatValue = (field: IFieldType, params: any[]) => - map(params, (val: any, key: string) => get(operators, key) + format(field, val)).join(' '); - -const format = (field: IFieldType, value: any) => - field && field.format && field.format.convert ? field.format.convert(value) : value; + map(params, (val: any, key: string) => get(operators, key) + val).join(' '); // Creates a filter where the value for the given field is in the given range // params should be an object containing `lt`, `lte`, `gt`, and/or `gte` diff --git a/src/plugins/data/common/search/expressions/phrase_filter.test.ts b/src/plugins/data/common/search/expressions/phrase_filter.test.ts index 39bd907513a0d..a61cc0bfd68ab 100644 --- a/src/plugins/data/common/search/expressions/phrase_filter.test.ts +++ b/src/plugins/data/common/search/expressions/phrase_filter.test.ts @@ -32,7 +32,6 @@ describe('interpreter/functions#phraseFilter', () => { "something", ], "type": "phrases", - "value": "test, something", }, "query": Object { "bool": Object { diff --git a/src/plugins/data/public/query/filter_manager/lib/get_display_value.test.ts b/src/plugins/data/public/query/filter_manager/lib/get_display_value.test.ts new file mode 100644 index 0000000000000..48e1007534769 --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/get_display_value.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { stubIndexPattern, phraseFilter } from 'src/plugins/data/common/stubs'; +import { getDisplayValueFromFilter } from './get_display_value'; + +describe('getDisplayValueFromFilter', () => { + it('returns the value if string', () => { + phraseFilter.meta.value = 'abc'; + const displayValue = getDisplayValueFromFilter(phraseFilter, [stubIndexPattern]); + expect(displayValue).toBe('abc'); + }); + + it('returns the value if undefined', () => { + phraseFilter.meta.value = undefined; + const displayValue = getDisplayValueFromFilter(phraseFilter, [stubIndexPattern]); + expect(displayValue).toBe(''); + }); + + it('calls the value function if proivided', () => { + // The type of value currently doesn't match how it's used. Refactor needed. + phraseFilter.meta.value = jest.fn((x) => { + return 'abc'; + }) as any; + const displayValue = getDisplayValueFromFilter(phraseFilter, [stubIndexPattern]); + expect(displayValue).toBe('abc'); + expect(phraseFilter.meta.value).toHaveBeenCalledWith(undefined); + }); + + it('calls the value function if proivided, with formatter', () => { + stubIndexPattern.getFormatterForField = jest.fn().mockReturnValue('banana'); + phraseFilter.meta.value = jest.fn((x) => { + return x + 'abc'; + }) as any; + const displayValue = getDisplayValueFromFilter(phraseFilter, [stubIndexPattern]); + expect(stubIndexPattern.getFormatterForField).toHaveBeenCalledTimes(1); + expect(phraseFilter.meta.value).toHaveBeenCalledWith('banana'); + expect(displayValue).toBe('bananaabc'); + }); +}); diff --git a/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts index 45c6167f600bc..1ccfaacb24e4b 100644 --- a/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts +++ b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts @@ -28,11 +28,14 @@ function getValueFormatter(indexPattern?: IIndexPattern, key?: string) { } export function getDisplayValueFromFilter(filter: Filter, indexPatterns: IIndexPattern[]): string { - if (typeof filter.meta.value === 'function') { + const { key, value } = filter.meta; + if (typeof value === 'function') { const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); - const valueFormatter: any = getValueFormatter(indexPattern, filter.meta.key); - return (filter.meta.value as any)(valueFormatter); + const valueFormatter = getValueFormatter(indexPattern, key); + // TODO: distinguish between FilterMeta which is serializable to mapped FilterMeta + // Where value can be a function. + return (value as any)(valueFormatter); } else { - return filter.meta.value || ''; + return value || ''; } } diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts index bfd528264b00f..5601dd66e5206 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_phrases.ts @@ -6,14 +6,29 @@ * Side Public License, v 1. */ -import { Filter, isPhrasesFilter } from '../../../../../common'; +import { Filter, FilterValueFormatter, isPhrasesFilter } from '../../../../../common'; + +const getFormattedValueFn = (params: any) => { + return (formatter?: FilterValueFormatter) => { + return params + .map((v: any) => { + return formatter ? formatter.convert(v) : v; + }) + .join(', '); + }; +}; export const mapPhrases = (filter: Filter) => { if (!isPhrasesFilter(filter)) { throw filter; } - const { type, key, value, params } = filter.meta; + const { type, key, params } = filter.meta; - return { type, key, value, params }; + return { + type, + key, + value: getFormattedValueFn(params), + params, + }; }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js index faf6fef0aa549..8f19644132d3f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -8,6 +8,7 @@ import _, { isArray, last, get } from 'lodash'; import React, { Component } from 'react'; +import { parse as parseUrl } from 'url'; import PropTypes from 'prop-types'; import { RedirectAppLinks } from '../../../../../../kibana_react/public'; import { createTickFormatter } from '../../lib/tick_formatter'; @@ -33,6 +34,14 @@ function getColor(rules, colorKey, value) { return color; } +function sanitizeUrl(url) { + // eslint-disable-next-line no-script-url + if (parseUrl(url).protocol === 'javascript:') { + return ''; + } + return url; +} + class TableVis extends Component { constructor(props) { super(props); @@ -52,7 +61,7 @@ class TableVis extends Component { let rowDisplay = model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key; if (model.drilldown_url) { const url = replaceVars(model.drilldown_url, {}, { key: row.key }); - rowDisplay = {rowDisplay}; + rowDisplay = {rowDisplay}; } const columns = row.series .filter((item) => item) diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 1dbb633e32adf..3edbd3443ffc1 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -23,6 +23,23 @@ export type StatusAllType = typeof StatusAll; export type CaseStatusWithAllStatus = CaseStatuses | StatusAllType; +/** + * The type for the `refreshRef` prop (a `React.Ref`) defined by the `CaseViewComponentProps`. + * + * @example + * const refreshRef = useRef(null); + * return + */ +export type CaseViewRefreshPropInterface = null | { + /** + * Refreshes the all of the user actions/comments in the view's timeline + * (note: this also triggers a silent `refreshCase()`) + */ + refreshUserActionsAndComments: () => Promise; + /** Refreshes the Case information only */ + refreshCase: () => Promise; +}; + export type Comment = CommentRequest & { associationType: AssociationType; id: string; diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 9c6e9442c8f56..d5b535b8ddad1 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef, MutableRefObject } from 'react'; import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; import { @@ -16,11 +16,19 @@ import { EuiHorizontalRule, } from '@elastic/eui'; -import { CaseStatuses, CaseAttributes, CaseType, Case, CaseConnector, Ecs } from '../../../common'; +import { + CaseStatuses, + CaseAttributes, + CaseType, + Case, + CaseConnector, + Ecs, + CaseViewRefreshPropInterface, +} from '../../../common'; import { HeaderPage } from '../header_page'; import { EditableTitle } from '../header_page/editable_title'; import { TagList } from '../tag_list'; -import { useGetCase } from '../../containers/use_get_case'; +import { UseGetCase, useGetCase } from '../../containers/use_get_case'; import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { useUpdateCase } from '../../containers/use_update_case'; @@ -42,6 +50,7 @@ import { getConnectorById } from '../utils'; import { DoesNotExist } from './does_not_exist'; const gutterTimeline = '70px'; // seems to be a timeline reference from the original file + export interface CaseViewComponentProps { allCasesNavigation: CasesNavigation; caseDetailsNavigation: CasesNavigation; @@ -54,12 +63,18 @@ export interface CaseViewComponentProps { subCaseId?: string; useFetchAlertData: (alertIds: string[]) => [boolean, Record]; userCanCrud: boolean; + /** + * A React `Ref` that Exposes data refresh callbacks. + * **NOTE**: Do not hold on to the `.current` object, as it could become stale + */ + refreshRef?: MutableRefObject; } export interface CaseViewProps extends CaseViewComponentProps { onCaseDataSuccess?: (data: Case) => void; timelineIntegration?: CasesTimelineIntegration; } + export interface OnUpdateFields { key: keyof Case; value: Case[keyof Case]; @@ -78,13 +93,14 @@ const MyEuiFlexGroup = styled(EuiFlexGroup)` const MyEuiHorizontalRule = styled(EuiHorizontalRule)` margin-left: 48px; + &.euiHorizontalRule--full { width: calc(100% - 48px); } `; export interface CaseComponentProps extends CaseViewComponentProps { - fetchCase: () => void; + fetchCase: UseGetCase['fetchCase']; caseData: Case; updateCase: (newCase: Case) => void; } @@ -105,6 +121,7 @@ export const CaseComponent = React.memo( updateCase, useFetchAlertData, userCanCrud, + refreshRef, }) => { const [initLoadingData, setInitLoadingData] = useState(true); const init = useRef(true); @@ -124,6 +141,51 @@ export const CaseComponent = React.memo( subCaseId, }); + // Set `refreshRef` if needed + useEffect(() => { + let isStale = false; + + if (refreshRef) { + refreshRef.current = { + refreshCase: async () => { + // Do nothing if component (or instance of this render cycle) is stale + if (isStale) { + return; + } + + await fetchCase(); + }, + refreshUserActionsAndComments: async () => { + // Do nothing if component (or instance of this render cycle) is stale + // -- OR -- + // it is already loading + if (isStale || isLoadingUserActions) { + return; + } + + await Promise.all([ + fetchCase(true), + fetchCaseUserActions(caseId, caseData.connector.id, subCaseId), + ]); + }, + }; + + return () => { + isStale = true; + refreshRef.current = null; + }; + } + }, [ + caseData.connector.id, + caseId, + fetchCase, + fetchCaseUserActions, + isLoadingUserActions, + refreshRef, + subCaseId, + updateCase, + ]); + // Update Fields const onUpdateField = useCallback( ({ key, value, onSuccess, onError }: OnUpdateFields) => { @@ -491,6 +553,7 @@ export const CaseView = React.memo( timelineIntegration, useFetchAlertData, userCanCrud, + refreshRef, }: CaseViewProps) => { const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId, subCaseId); if (isError) { @@ -528,6 +591,7 @@ export const CaseView = React.memo( updateCase={updateCase} useFetchAlertData={useFetchAlertData} userCanCrud={userCanCrud} + refreshRef={refreshRef} /> diff --git a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx index 75d9ac74a8ccf..c88f530709c8a 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx @@ -89,6 +89,19 @@ describe('useGetCase', () => { }); }); + it('set isLoading to false when refetching case "silent"ly', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchCase(true); + + expect(result.current.isLoading).toBe(false); + }); + }); + it('unhappy path', async () => { const spyOnGetCase = jest.spyOn(api, 'getCase'); spyOnGetCase.mockImplementation(() => { diff --git a/x-pack/plugins/cases/public/containers/use_get_case.tsx b/x-pack/plugins/cases/public/containers/use_get_case.tsx index 7b59f8e06b7af..b9326ad057c9e 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.tsx @@ -19,7 +19,7 @@ interface CaseState { } type Action = - | { type: 'FETCH_INIT' } + | { type: 'FETCH_INIT'; payload: { silent: boolean } } | { type: 'FETCH_SUCCESS'; payload: Case } | { type: 'FETCH_FAILURE' } | { type: 'UPDATE_CASE'; payload: Case }; @@ -29,7 +29,10 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { case 'FETCH_INIT': return { ...state, - isLoading: true, + // If doing a silent fetch, then don't set `isLoading`. This helps + // with preventing screen flashing when wanting to refresh the actions + // and comments + isLoading: !action.payload?.silent, isError: false, }; case 'FETCH_SUCCESS': @@ -56,7 +59,11 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { }; export interface UseGetCase extends CaseState { - fetchCase: () => void; + /** + * @param [silent] When set to `true`, the `isLoading` property will not be set to `true` + * while doing the API call + */ + fetchCase: (silent?: boolean) => Promise; updateCase: (newCase: Case) => void; } @@ -74,33 +81,35 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { dispatch({ type: 'UPDATE_CASE', payload: newCase }); }, []); - const callFetch = useCallback(async () => { - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - dispatch({ type: 'FETCH_INIT' }); + const callFetch = useCallback( + async (silent: boolean = false) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + dispatch({ type: 'FETCH_INIT', payload: { silent } }); - const response = await (subCaseId - ? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal) - : getCase(caseId, true, abortCtrlRef.current.signal)); + const response = await (subCaseId + ? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal) + : getCase(caseId, true, abortCtrlRef.current.signal)); - if (!isCancelledRef.current) { - dispatch({ type: 'FETCH_SUCCESS', payload: response }); - } - } catch (error) { - if (!isCancelledRef.current) { - if (error.name !== 'AbortError') { - toasts.addError( - error.body && error.body.message ? new Error(error.body.message) : error, - { title: i18n.ERROR_TITLE } - ); + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS', payload: response }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + toasts.addError( + error.body && error.body.message ? new Error(error.body.message) : error, + { title: i18n.ERROR_TITLE } + ); + } + dispatch({ type: 'FETCH_FAILURE' }); } - dispatch({ type: 'FETCH_FAILURE' }); } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [caseId, subCaseId]); + }, + [caseId, subCaseId, toasts] + ); useEffect(() => { callFetch(); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx index 66aa93154b318..edafa1b9a10a9 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx @@ -51,7 +51,11 @@ export const initialData: CaseUserActionsState = { }; export interface UseGetCaseUserActions extends CaseUserActionsState { - fetchCaseUserActions: (caseId: string, caseConnectorId: string, subCaseId?: string) => void; + fetchCaseUserActions: ( + caseId: string, + caseConnectorId: string, + subCaseId?: string + ) => Promise; } const getExternalService = (value: string): CaseExternalService | null => diff --git a/x-pack/plugins/enterprise_search/jest.config.js b/x-pack/plugins/enterprise_search/jest.config.js index be9f7b9939a66..7d10d7aa87bf2 100644 --- a/x-pack/plugins/enterprise_search/jest.config.js +++ b/x-pack/plugins/enterprise_search/jest.config.js @@ -15,5 +15,6 @@ module.exports = { '/x-pack/plugins/enterprise_search/**/*.{ts,tsx}', '!/x-pack/plugins/enterprise_search/public/*.ts', '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', ], }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx index aab2909d630ed..fc5ad0cb06411 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx @@ -28,8 +28,17 @@ const domains: CrawlerDomain[] = [ crawlRules: [], entryPoints: [], sitemaps: [], - lastCrawl: '2020-01-01T00:00:00-05:00', - createdOn: '2020-01-01T00:00:00-05:00', + lastCrawl: '2020-01-01T00:00:00-12:00', + createdOn: '2020-01-01T00:00:00-12:00', + }, + { + id: '4567', + documentCount: 0, + url: 'empty.site', + crawlRules: [], + entryPoints: [], + sitemaps: [], + createdOn: '1970-01-01T00:00:00-12:00', }, ]; @@ -68,15 +77,6 @@ describe('DomainsTable', () => { }); describe('columns', () => { - const getTable = () => wrapper.find(EuiInMemoryTable).dive().find(EuiBasicTable).dive(); - - beforeEach(() => { - wrapper = shallow(); - tableContent = mountWithIntl() - .find(EuiInMemoryTable) - .text(); - }); - it('renders a url column', () => { expect(tableContent).toContain('elastic.co'); }); @@ -92,8 +92,9 @@ describe('DomainsTable', () => { }); describe('actions column', () => { + const getTable = () => wrapper.find(EuiInMemoryTable).dive().find(EuiBasicTable).dive(); const getActions = () => getTable().find('ExpandedItemActions'); - const getActionItems = () => getActions().dive().find('DefaultItemAction'); + const getActionItems = () => getActions().first().dive().find('DefaultItemAction'); it('will hide the action buttons if the user cannot manage/delete engines', () => { setMockValues({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts index 7ef5984960e26..146f31eab8a97 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts @@ -55,7 +55,7 @@ const MOCK_CLIENT_DATA = crawlerDataServerToClient(MOCK_SERVER_DATA); describe('CrawlerOverviewLogic', () => { const { mount } = new LogicMounter(CrawlerOverviewLogic); const { http } = mockHttpValues; - const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; beforeEach(() => { jest.clearAllMocks(); @@ -138,7 +138,7 @@ describe('CrawlerOverviewLogic', () => { expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith( MOCK_CLIENT_DATA ); - expect(setSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); }); it('calls flashApiErrors when there is an error', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts index dceb4e205487d..3f64cbbd9a866 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts @@ -9,7 +9,7 @@ import { kea, MakeLogicType } from 'kea'; import { i18n } from '@kbn/i18n'; -import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages'; +import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { EngineLogic } from '../engine'; @@ -21,7 +21,7 @@ export const DELETE_DOMAIN_MESSAGE = (domainUrl: string) => i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.delete.successMessage', { - defaultMessage: 'Successfully deleted "{domainUrl}"', + defaultMessage: "Domain '{domainUrl}' was deleted", values: { domainUrl, }, @@ -90,7 +90,7 @@ export const CrawlerOverviewLogic = kea< ); const crawlerData = crawlerDataServerToClient(response); actions.onReceiveCrawlerData(crawlerData); - setSuccessMessage(DELETE_DOMAIN_MESSAGE(domain.url)); + flashSuccessToast(DELETE_DOMAIN_MESSAGE(domain.url)); } catch (e) { flashAPIErrors(e); } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index 0fb118548a67b..6a5f3df0e86f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -20,15 +20,21 @@ export enum ApiTokenTypes { Search = 'search', } -export const CREATE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.created', { - defaultMessage: 'Successfully created key.', -}); -export const UPDATE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.update', { - defaultMessage: 'Successfully updated API Key.', -}); -export const DELETE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.deleted', { - defaultMessage: 'Successfully deleted key.', -}); +export const CREATE_MESSAGE = (name: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.tokens.created', { + defaultMessage: "API key '{name}' was created", + values: { name }, + }); +export const UPDATE_MESSAGE = (name: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.tokens.update', { + defaultMessage: "API key '{name}' was updated", + values: { name }, + }); +export const DELETE_MESSAGE = (name: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.tokens.deleted', { + defaultMessage: "API key '{name}' was deleted", + values: { name }, + }); export const SEARCH_DISPLAY = i18n.translate( 'xpack.enterpriseSearch.appSearch.tokens.permissions.display.search', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index 7192488e49746..a12c174b9478c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -31,7 +31,7 @@ import { CredentialsLogic } from './credentials_logic'; describe('CredentialsLogic', () => { const { mount } = new LogicMounter(CredentialsLogic); const { http } = mockHttpValues; - const { clearFlashMessages, setSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers; const DEFAULT_VALUES = { activeApiToken: { @@ -1110,7 +1110,7 @@ describe('CredentialsLogic', () => { await nextTick(); expect(CredentialsLogic.actions.fetchCredentials).toHaveBeenCalled(); - expect(setSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); }); it('handles errors', async () => { @@ -1142,7 +1142,7 @@ describe('CredentialsLogic', () => { }); await nextTick(); expect(CredentialsLogic.actions.onApiTokenCreateSuccess).toHaveBeenCalledWith(createdToken); - expect(setSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); }); it('calls a PUT endpoint that updates the active token if it already exists', async () => { @@ -1169,7 +1169,7 @@ describe('CredentialsLogic', () => { }); await nextTick(); expect(CredentialsLogic.actions.onApiTokenUpdateSuccess).toHaveBeenCalledWith(updatedToken); - expect(setSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); }); it('handles errors', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts index 2841282704189..60282bc0a3316 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts @@ -11,7 +11,7 @@ import { Meta } from '../../../../../common/types'; import { DEFAULT_META } from '../../../shared/constants'; import { clearFlashMessages, - setSuccessMessage, + flashSuccessToast, flashAPIErrors, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; @@ -261,7 +261,7 @@ export const CredentialsLogic = kea({ await http.delete(`/api/app_search/credentials/${tokenName}`); actions.fetchCredentials(); - setSuccessMessage(DELETE_MESSAGE); + flashSuccessToast(DELETE_MESSAGE(tokenName)); } catch (e) { flashAPIErrors(e); } @@ -289,11 +289,11 @@ export const CredentialsLogic = kea({ if (id) { const response = await http.put(`/api/app_search/credentials/${name}`, { body }); actions.onApiTokenUpdateSuccess(response); - setSuccessMessage(UPDATE_MESSAGE); + flashSuccessToast(UPDATE_MESSAGE(name)); } else { const response = await http.post('/api/app_search/credentials', { body }); actions.onApiTokenCreateSuccess(response); - setSuccessMessage(CREATE_MESSAGE); + flashSuccessToast(CREATE_MESSAGE(name)); } } catch (e) { flashAPIErrors(e); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts index c490910184a69..f8c3e3efdbc1d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts @@ -40,7 +40,7 @@ export const DELETE_MESSAGE = i18n.translate( ); export const SUCCESS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.deleteSuccessMessage', - { defaultMessage: 'Successfully removed curation.' } + { defaultMessage: 'Your curation was deleted' } ); export const RESTORE_CONFIRMATION = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.restoreConfirmation', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts index f00f744f730ab..9268da8eb02b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts @@ -23,7 +23,7 @@ describe('CurationsLogic', () => { const { mount } = new LogicMounter(CurationsLogic); const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { clearFlashMessages, setSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers; const MOCK_CURATIONS_RESPONSE = { meta: { @@ -154,7 +154,7 @@ describe('CurationsLogic', () => { '/api/app_search/engines/some-engine/curations/some-curation-id' ); expect(CurationsLogic.actions.loadCurations).toHaveBeenCalled(); - expect(setSuccessMessage).toHaveBeenCalledWith('Successfully removed curation.'); + expect(flashSuccessToast).toHaveBeenCalledWith('Your curation was deleted'); }); it('handles errors', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts index f4916f54fbc22..89d170a83a4c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts @@ -11,7 +11,7 @@ import { Meta } from '../../../../../common/types'; import { DEFAULT_META } from '../../../shared/constants'; import { clearFlashMessages, - setSuccessMessage, + flashSuccessToast, flashAPIErrors, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; @@ -95,7 +95,7 @@ export const CurationsLogic = kea { const { mount } = new LogicMounter(DocumentDetailLogic); const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { setQueuedSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; + const { flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers; const DEFAULT_VALUES = { dataLoading: true, @@ -101,9 +101,9 @@ describe('DocumentDetailLogic', () => { expect(http.delete).toHaveBeenCalledWith('/api/app_search/engines/engine1/documents/1'); await nextTick(); - expect(setQueuedSuccessMessage).toHaveBeenCalledWith( - 'Successfully marked document for deletion. It will be deleted momentarily.' - ); + expect(flashSuccessToast).toHaveBeenCalledWith('Your document was marked for deletion', { + text: 'It will be deleted momentarily.', + }); expect(navigateToUrl).toHaveBeenCalledWith('/engines/engine1/documents'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts index 17c2c788523d0..c7a68ec0f8131 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts @@ -9,7 +9,7 @@ import { kea, MakeLogicType } from 'kea'; import { i18n } from '@kbn/i18n'; -import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; +import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; @@ -79,19 +79,20 @@ export const DocumentDetailLogic = kea({ 'xpack.enterpriseSearch.appSearch.documentDetail.confirmDelete', { defaultMessage: 'Are you sure you want to delete this document?' } ); - const DELETE_SUCCESS = i18n.translate( + const DELETE_SUCCESS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.documentDetail.deleteSuccess', - { - defaultMessage: - 'Successfully marked document for deletion. It will be deleted momentarily.', - } + { defaultMessage: 'Your document was marked for deletion' } + ); + const DELETE_SUCCESS_TEXT = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentDetail.deleteSuccessDescription', + { defaultMessage: 'It will be deleted momentarily.' } ); if (window.confirm(CONFIRM_DELETE)) { try { const { http } = HttpLogic.values; await http.delete(`/api/app_search/engines/${engineName}/documents/${documentId}`); - setQueuedSuccessMessage(DELETE_SUCCESS); + flashSuccessToast(DELETE_SUCCESS_TITLE, { text: DELETE_SUCCESS_TEXT }); navigateToUrl(generateEnginePath(ENGINE_DOCUMENTS_PATH)); } catch (e) { flashAPIErrors(e); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/constants.ts index 0647eeba78786..1f28be2282562 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/constants.ts @@ -65,12 +65,11 @@ export const ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL = i18n.translate( } ); -export const ENGINE_CREATION_SUCCESS_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engineCreation.successMessage', - { - defaultMessage: 'Successfully created engine.', - } -); +export const ENGINE_CREATION_SUCCESS_MESSAGE = (name: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.engineCreation.successMessage', { + defaultMessage: "Engine '{name}' was created", + values: { name }, + }); export const SUPPORTED_LANGUAGES = [ { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts index 1e87b3ef21f03..2001ac3646e5f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts @@ -20,7 +20,7 @@ describe('EngineCreationLogic', () => { const { mount } = new LogicMounter(EngineCreationLogic); const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { setQueuedSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; + const { flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers; const DEFAULT_VALUES = { isLoading: false, @@ -99,8 +99,8 @@ describe('EngineCreationLogic', () => { jest.clearAllMocks(); }); - it('should set a success message', () => { - expect(setQueuedSuccessMessage).toHaveBeenCalledWith('Successfully created engine.'); + it('should show a success message', () => { + expect(flashSuccessToast).toHaveBeenCalledWith("Engine 'test' was created"); }); it('should navigate the user to the engine page', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts index 844cd8fb4088a..4e41cdf4c3949 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts @@ -9,7 +9,7 @@ import { generatePath } from 'react-router-dom'; import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; +import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_PATH } from '../../routes'; @@ -85,7 +85,7 @@ export const EngineCreationLogic = kea i18n.translate( 'xpack.enterpriseSearch.appSearch.enginesOverview.table.action.delete.successMessage', { - defaultMessage: 'Successfully deleted "{engineName}"', + defaultMessage: "Engine '{engineName}' was deleted", values: { engineName, }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts index eb95d1ce148da..c699f6cf2eb43 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts @@ -22,7 +22,7 @@ import { EnginesLogic } from './'; describe('EnginesLogic', () => { const { mount } = new LogicMounter(EnginesLogic); const { http } = mockHttpValues; - const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; const DEFAULT_VALUES = { dataLoading: true, @@ -199,10 +199,10 @@ describe('EnginesLogic', () => { mount(); }); - it('should call setSuccessMessage', () => { + it('should call flashSuccessToast', () => { EnginesLogic.actions.onDeleteEngineSuccess(MOCK_ENGINE); - expect(setSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); }); it('should call loadEngines if engine.type === default', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.ts index 36c31f9891f6e..a33f317d71e52 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.ts @@ -9,7 +9,7 @@ import { kea, MakeLogicType } from 'kea'; import { Meta } from '../../../../../common/types'; import { DEFAULT_META } from '../../../shared/constants'; -import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages'; +import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { updateMetaPageIndex } from '../../../shared/table_pagination'; @@ -141,7 +141,7 @@ export const EnginesLogic = kea>({ actions.onMetaEnginesLoad(response); }, onDeleteEngineSuccess: async ({ engine }) => { - setSuccessMessage(DELETE_ENGINE_MESSAGE(engine.name)); + flashSuccessToast(DELETE_ENGINE_MESSAGE(engine.name)); if ([EngineTypes.default, EngineTypes.indexed].includes(engine.type)) { actions.loadEngines(); } else if (engine.type === EngineTypes.meta) { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/constants.tsx index aff5942d497a8..9659fa3f2dfc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/constants.tsx @@ -111,9 +111,8 @@ export const META_ENGINE_CREATION_FORM_MAX_SOURCE_ENGINES_WARNING_TITLE = ( } ); -export const META_ENGINE_CREATION_SUCCESS_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.metaEngineCreation.successMessage', - { - defaultMessage: 'Successfully created meta engine.', - } -); +export const META_ENGINE_CREATION_SUCCESS_MESSAGE = (name: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.metaEngineCreation.successMessage', { + defaultMessage: "Meta engine '{name}' was created", + values: { name }, + }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts index 137a8afc5c4fe..6434e8a7df99a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts @@ -20,7 +20,7 @@ describe('MetaEngineCreationLogic', () => { const { mount } = new LogicMounter(MetaEngineCreationLogic); const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { setQueuedSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; + const { flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers; const DEFAULT_VALUES = { isLoading: false, @@ -152,8 +152,8 @@ describe('MetaEngineCreationLogic', () => { MetaEngineCreationLogic.actions.onEngineCreationSuccess(); }); - it('should set a success message', () => { - expect(setQueuedSuccessMessage).toHaveBeenCalledWith('Successfully created meta engine.'); + it('should show a success message', () => { + expect(flashSuccessToast).toHaveBeenCalledWith("Meta engine 'test' was created"); }); it('should navigate the user to the engine page', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts index 472a0dee12b7f..4df3bc4bff162 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts @@ -11,7 +11,7 @@ import { kea, MakeLogicType } from 'kea'; import { Meta } from '../../../../../common/types'; import { DEFAULT_META } from '../../../shared/constants'; -import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; +import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_PATH } from '../../routes'; @@ -113,7 +113,7 @@ export const MetaEngineCreationLogic = kea< const { navigateToUrl } = KibanaLogic.values; const enginePath = generatePath(ENGINE_PATH, { engineName: name }); - setQueuedSuccessMessage(META_ENGINE_CREATION_SUCCESS_MESSAGE); + flashSuccessToast(META_ENGINE_CREATION_SUCCESS_MESSAGE(name)); navigateToUrl(enginePath); }, submitEngine: async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts index 796bc9242dd98..105ab8b9618b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts @@ -27,16 +27,22 @@ export const RELEVANCE_TUNING_TITLE = i18n.translate( export const UPDATE_SUCCESS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.updateSuccess', { - defaultMessage: 'Relevance successfully tuned. The changes will impact your results shortly.', + defaultMessage: 'Relevance was tuned', } ); export const DELETE_SUCCESS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteSuccess', { - defaultMessage: - 'Relevance has been reset to default values. The change will impact your results shortly.', + defaultMessage: 'Relevance was reset to default values', } ); +export const SUCCESS_CHANGES_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.successDescription', + { + defaultMessage: 'The changes will impact your results shortly.', + } +); + export const RESET_CONFIRMATION_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.resetConfirmation', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index 1d143ed0ca938..8cb5b3b35d97c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -229,7 +229,7 @@ describe('RelevanceTuningLogic', () => { describe('listeners', () => { const { http } = mockHttpValues; - const { flashAPIErrors, setSuccessMessage, clearFlashMessages } = mockFlashMessageHelpers; + const { flashAPIErrors, flashSuccessToast, clearFlashMessages } = mockFlashMessageHelpers; let scrollToSpy: jest.SpyInstance; let confirmSpy: jest.SpyInstance; @@ -521,9 +521,9 @@ describe('RelevanceTuningLogic', () => { body: JSON.stringify(searchSettingsWithoutNewBoostProp), } ); - expect(setSuccessMessage).toHaveBeenCalledWith( - 'Relevance successfully tuned. The changes will impact your results shortly.' - ); + expect(flashSuccessToast).toHaveBeenCalledWith('Relevance was tuned', { + text: 'The changes will impact your results shortly.', + }); expect(RelevanceTuningLogic.actions.onSearchSettingsSuccess).toHaveBeenCalledWith( searchSettingsWithoutNewBoostProp ); @@ -577,9 +577,9 @@ describe('RelevanceTuningLogic', () => { expect(http.post).toHaveBeenCalledWith( '/api/app_search/engines/test-engine/search_settings/reset' ); - expect(setSuccessMessage).toHaveBeenCalledWith( - 'Relevance has been reset to default values. The change will impact your results shortly.' - ); + expect(flashSuccessToast).toHaveBeenCalledWith('Relevance was reset to default values', { + text: 'The changes will impact your results shortly.', + }); expect(RelevanceTuningLogic.actions.onSearchSettingsSuccess).toHaveBeenCalledWith( searchSettings ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index b3c795d14b8e1..00896c923616b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -9,7 +9,7 @@ import { kea, MakeLogicType } from 'kea'; import { omit, cloneDeep, isEmpty } from 'lodash'; import { - setSuccessMessage, + flashSuccessToast, flashAPIErrors, clearFlashMessages, } from '../../../shared/flash_messages'; @@ -24,6 +24,7 @@ import { RESET_CONFIRMATION_MESSAGE, DELETE_SUCCESS_MESSAGE, DELETE_CONFIRMATION_MESSAGE, + SUCCESS_CHANGES_MESSAGE, BOOST_TYPE_TO_EMPTY_BOOST, } from './constants'; import { Boost, BoostFunction, BoostOperation, BoostType, SearchSettings } from './types'; @@ -304,7 +305,7 @@ export const RelevanceTuningLogic = kea< const response = await http.put(url, { body: JSON.stringify(removeBoostStateProps(values.searchSettings)), }); - setSuccessMessage(UPDATE_SUCCESS_MESSAGE); + flashSuccessToast(UPDATE_SUCCESS_MESSAGE, { text: SUCCESS_CHANGES_MESSAGE }); actions.onSearchSettingsSuccess(response); } catch (e) { flashAPIErrors(e); @@ -326,7 +327,7 @@ export const RelevanceTuningLogic = kea< try { const response = await http.post(url); - setSuccessMessage(DELETE_SUCCESS_MESSAGE); + flashSuccessToast(DELETE_SUCCESS_MESSAGE, { text: SUCCESS_CHANGES_MESSAGE }); actions.onSearchSettingsSuccess(response); } catch (e) { flashAPIErrors(e); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts index e7ac94e9f9d2d..13530c2c29ef0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts @@ -10,7 +10,7 @@ import { omit, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages'; +import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { Schema, SchemaConflicts } from '../../../shared/schema/types'; import { EngineLogic } from '../engine'; @@ -333,11 +333,11 @@ export const ResultSettingsLogic = kea { const { http } = mockHttpValues; - const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; const { mount } = new LogicMounter(RoleMappingsLogic); const DEFAULT_VALUES = { attributes: [], @@ -519,7 +519,7 @@ describe('RoleMappingsLogic', () => { await nextTick(); expect(initializeRoleMappingsSpy).toHaveBeenCalled(); - expect(setSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); }); it('sends array when "accessAllEngines" is false', () => { @@ -670,7 +670,7 @@ describe('RoleMappingsLogic', () => { await nextTick(); expect(initializeRoleMappingsSpy).toHaveBeenCalled(); - expect(setSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); }); it('handles error', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts index fb574a3588989..8647f4f1df357 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -10,7 +10,7 @@ import { kea, MakeLogicType } from 'kea'; import { clearFlashMessages, flashAPIErrors, - setSuccessMessage, + flashSuccessToast, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { @@ -396,7 +396,7 @@ export const RoleMappingsLogic = kea { const { mount } = new LogicMounter(SampleEngineCreationCtaLogic); const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { setQueuedSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; + const { flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers; const DEFAULT_VALUES = { isLoading: false, @@ -82,10 +82,10 @@ describe('SampleEngineCreationCtaLogic', () => { }); }); - it('onSampleEngineCreationSuccess should set a success message and navigate the user to the engine page', () => { + it('onSampleEngineCreationSuccess should show a success message and navigate the user to the engine page', () => { SampleEngineCreationCtaLogic.actions.onSampleEngineCreationSuccess(); - expect(setQueuedSuccessMessage).toHaveBeenCalledWith('Successfully created engine.'); + expect(flashSuccessToast).toHaveBeenCalledWith("Engine 'national-parks-demo' was created"); expect(navigateToUrl).toHaveBeenCalledWith('/engines/national-parks-demo'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta_logic.ts index 37570d4e3cfe7..bea85ea4fb3a5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta_logic.ts @@ -9,7 +9,7 @@ import { generatePath } from 'react-router-dom'; import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; +import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_PATH } from '../../routes'; @@ -65,7 +65,7 @@ export const SampleEngineCreationCtaLogic = kea< const { navigateToUrl } = KibanaLogic.values; const enginePath = generatePath(ENGINE_PATH, { engineName: 'national-parks-demo' }); - setQueuedSuccessMessage(ENGINE_CREATION_SUCCESS_MESSAGE); + flashSuccessToast(ENGINE_CREATION_SUCCESS_MESSAGE('national-parks-demo')); navigateToUrl(enginePath); }, }), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts index 4e3f4f81d5a9f..66421f218a44f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts @@ -37,7 +37,7 @@ export const ADD_SOURCE_ENGINES_SUCCESS_MESSAGE = (sourceEngineNames: string[]) 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesSuccessMessage', { defaultMessage: - '{sourceEnginesCount, plural, one {# engine has} other {# engines have}} been added to this meta engine.', + '{sourceEnginesCount, plural, one {# engine was} other {# engines were}} added to this meta engine', values: { sourceEnginesCount: sourceEngineNames.length }, } ); @@ -61,7 +61,7 @@ export const REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE = (engineName: string) => i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.souceEngines.removeSourceEngineSuccessMessage', { - defaultMessage: 'Engine {engineName} has been removed from this meta engine.', + defaultMessage: "Engine '{engineName}' was removed from this meta engine", values: { engineName }, } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts index c39a25276a43c..eababb9d93c58 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts @@ -23,7 +23,7 @@ import { SourceEnginesLogic } from './source_engines_logic'; describe('SourceEnginesLogic', () => { const { http } = mockHttpValues; const { mount } = new LogicMounter(SourceEnginesLogic); - const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; const DEFAULT_VALUES = { dataLoading: true, @@ -292,8 +292,8 @@ describe('SourceEnginesLogic', () => { { name: 'source-engine-3' }, { name: 'source-engine-4' }, ]); - expect(setSuccessMessage).toHaveBeenCalledWith( - '2 engines have been added to this meta engine.' + expect(flashSuccessToast).toHaveBeenCalledWith( + '2 engines were added to this meta engine' ); }); @@ -352,8 +352,8 @@ describe('SourceEnginesLogic', () => { SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); await nextTick(); - expect(setSuccessMessage).toHaveBeenCalledWith( - 'Engine source-engine-2 has been removed from this meta engine.' + expect(flashSuccessToast).toHaveBeenCalledWith( + "Engine 'source-engine-2' was removed from this meta engine" ); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts index c10f11a7de327..21ae24357bcea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts @@ -7,7 +7,7 @@ import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages'; +import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { recursivelyFetchEngines } from '../../utils/recursively_fetch_engines'; import { EngineLogic } from '../engine'; @@ -143,7 +143,7 @@ export const SourceEnginesLogic = kea< ); actions.onSourceEnginesAdd(sourceEnginesToAdd); - setSuccessMessage(ADD_SOURCE_ENGINES_SUCCESS_MESSAGE(sourceEngineNames)); + flashSuccessToast(ADD_SOURCE_ENGINES_SUCCESS_MESSAGE(sourceEngineNames)); EngineLogic.actions.initializeEngine(); } catch (e) { flashAPIErrors(e); @@ -176,7 +176,7 @@ export const SourceEnginesLogic = kea< ); actions.onSourceEngineRemove(sourceEngineName); - setSuccessMessage(REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE(sourceEngineName)); + flashSuccessToast(REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE(sourceEngineName)); // Changing source engines can change schema conflicts and invalid boosts, // so we re-initialize the engine to re-fetch that data diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 414957656467a..67ec06970a77e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -20,7 +20,7 @@ import { InitialAppData } from '../../common/types'; import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; import { externalUrl } from './shared/enterprise_search_url'; -import { mountFlashMessagesLogic } from './shared/flash_messages'; +import { mountFlashMessagesLogic, Toasts } from './shared/flash_messages'; import { mountHttpLogic } from './shared/http'; import { mountKibanaLogic } from './shared/kibana'; import { mountLicensingLogic } from './shared/licensing'; @@ -45,10 +45,10 @@ export const renderApp = ( const unmountKibanaLogic = mountKibanaLogic({ config, charts: plugins.charts, - cloud: plugins.cloud || {}, + cloud: plugins.cloud, history: params.history, navigateToUrl: core.application.navigateToUrl, - security: plugins.security || {}, + security: plugins.security, setBreadcrumbs: core.chrome.setBreadcrumbs, setChromeIsVisible: core.chrome.setIsVisible, setDocTitle: core.chrome.docTitle.change, @@ -71,6 +71,7 @@ export const renderApp = ( + , diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx index 6f4f3853fa8c9..757e5509773ac 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx @@ -13,88 +13,76 @@ import { shallow } from 'enzyme'; import { EuiCallOut, EuiGlobalToastList } from '@elastic/eui'; -import { FlashMessages, Callouts, Toasts } from './flash_messages'; +import { FlashMessages, Toasts } from './flash_messages'; describe('FlashMessages', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); + it('renders an array of callouts', () => { + const mockMessages = [ + { type: 'success', message: 'Hello world!!' }, + { + type: 'error', + message: 'Whoa nelly!', + description:
Something went wrong
, + }, + { type: 'info', message: 'Everything is fine, nothing is ruined' }, + { type: 'warning', message: 'Uh oh' }, + { type: 'info', message: 'Testing multiples of same type' }, + ]; + setMockValues({ messages: mockMessages }); - it('renders callout and toast flash messages', () => { const wrapper = shallow(); - expect(wrapper.find(Callouts)).toHaveLength(1); - expect(wrapper.find(Toasts)).toHaveLength(1); - }); - - describe('callouts', () => { - it('renders an array of flash messages & types', () => { - const mockMessages = [ - { type: 'success', message: 'Hello world!!' }, - { - type: 'error', - message: 'Whoa nelly!', - description:
Something went wrong
, - }, - { type: 'info', message: 'Everything is fine, nothing is ruined' }, - { type: 'warning', message: 'Uh oh' }, - { type: 'info', message: 'Testing multiples of same type' }, - ]; - setMockValues({ messages: mockMessages }); - - const wrapper = shallow(); - - expect(wrapper.find(EuiCallOut)).toHaveLength(5); - expect(wrapper.find(EuiCallOut).first().prop('color')).toEqual('success'); - expect(wrapper.find('[data-test-subj="error"]')).toHaveLength(1); - expect(wrapper.find(EuiCallOut).last().prop('iconType')).toEqual('iInCircle'); - }); - it('renders any children', () => { - setMockValues({ messages: [{ type: 'success' }] }); - - const wrapper = shallow( - - - - ); - - expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action'); - }); + expect(wrapper.find(EuiCallOut)).toHaveLength(5); + expect(wrapper.find(EuiCallOut).first().prop('color')).toEqual('success'); + expect(wrapper.find('[data-test-subj="error"]')).toHaveLength(1); + expect(wrapper.find(EuiCallOut).last().prop('iconType')).toEqual('iInCircle'); }); - describe('toasts', () => { - const actions = { dismissToastMessage: jest.fn() }; - beforeAll(() => setMockActions(actions)); + it('renders any children', () => { + setMockValues({ messages: [{ type: 'success' }] }); - it('renders an EUI toast list', () => { - const mockToasts = [ - { id: 'test', title: 'Hello world!!' }, - { - color: 'success', - iconType: 'check', - title: 'Success!', - toastLifeTimeMs: 500, - id: 'successToastId', - }, - { - color: 'danger', - iconType: 'alert', - title: 'Oh no!', - text:
Something went wrong
, - id: 'errorToastId', - }, - ]; - setMockValues({ toastMessages: mockToasts }); + const wrapper = shallow( + + + + ); - const wrapper = shallow(); - const euiToastList = wrapper.find(EuiGlobalToastList); + expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action'); + }); +}); - expect(euiToastList).toHaveLength(1); - expect(euiToastList.prop('toasts')).toEqual(mockToasts); - expect(euiToastList.prop('dismissToast')).toEqual(actions.dismissToastMessage); - expect(euiToastList.prop('toastLifeTimeMs')).toEqual(5000); - }); +describe('Toasts', () => { + const actions = { dismissToastMessage: jest.fn() }; + beforeAll(() => setMockActions(actions)); + + it('renders an EUI toast list', () => { + const mockToasts = [ + { id: 'test', title: 'Hello world!!' }, + { + color: 'success', + iconType: 'check', + title: 'Success!', + toastLifeTimeMs: 500, + id: 'successToastId', + }, + { + color: 'danger', + iconType: 'alert', + title: 'Oh no!', + text:
Something went wrong
, + id: 'errorToastId', + }, + ]; + setMockValues({ toastMessages: mockToasts }); + + const wrapper = shallow(); + const euiToastList = wrapper.find(EuiGlobalToastList); + + expect(euiToastList).toHaveLength(1); + expect(euiToastList.prop('toasts')).toEqual(mockToasts); + expect(euiToastList.prop('dismissToast')).toEqual(actions.dismissToastMessage); + expect(euiToastList.prop('toastLifeTimeMs')).toEqual(5000); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx index cf4e1a2d6cabc..ba42b89d6ab56 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx @@ -15,14 +15,7 @@ import { i18n } from '@kbn/i18n'; import { FLASH_MESSAGE_TYPES, DEFAULT_TOAST_TIMEOUT } from './constants'; import { FlashMessagesLogic } from './flash_messages_logic'; -export const FlashMessages: React.FC = ({ children }) => ( - <> - {children} - - -); - -export const Callouts: React.FC = ({ children }) => { +export const FlashMessages: React.FC = ({ children }) => { const { messages } = useValues(FlashMessagesLogic); return ( @@ -51,6 +44,11 @@ export const Callouts: React.FC = ({ children }) => { ); }; +/* + * NOTE: Toasts are rendered at the highest app level (@see public/applications/index.tsx) + * so that they don't rerender/reset their timers when navigating between pages, + * and also to prevent z-index issues with flyouts and modals + */ export const Toasts: React.FC = () => { const { toastMessages } = useValues(FlashMessagesLogic); const { dismissToastMessage } = useActions(FlashMessagesLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts index f08ac493f20b3..38a5d6e8b0b30 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { FlashMessages } from './flash_messages'; +export { FlashMessages, Toasts } from './flash_messages'; export { FlashMessagesLogic, mountFlashMessagesLogic } from './flash_messages_logic'; export { IFlashMessage } from './types'; export { flashAPIErrors } from './handle_api_errors'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 9c6db7d09f72c..d3b76f8dee9f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -20,18 +20,23 @@ import { createHref, CreateHrefOptions } from '../react_router_helpers'; interface KibanaLogicProps { config: { host?: string }; + // Kibana core history: History; - cloud: Partial; - charts: ChartsPluginStart; navigateToUrl: ApplicationStart['navigateToUrl']; - security: Partial; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; setChromeIsVisible(isVisible: boolean): void; setDocTitle(title: string): void; renderHeaderActions(HeaderActions: FC): void; + // Required plugins + charts: ChartsPluginStart; + // Optional plugins + cloud?: CloudSetup; + security?: SecurityPluginStart; } -export interface KibanaValues extends KibanaLogicProps { +export interface KibanaValues extends Omit { navigateToUrl(path: string, options?: CreateHrefOptions): Promise; + cloud: Partial; + security: Partial; } export const KibanaLogic = kea>({ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 5a90dd2c4a6bb..4743e808cc6ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -38,8 +38,7 @@ export interface RoleMapping { }; } -const productNames = [APP_SEARCH_PLUGIN.NAME, WORKPLACE_SEARCH_PLUGIN.NAME] as const; -export type ProductName = typeof productNames[number]; +export type ProductName = typeof APP_SEARCH_PLUGIN.NAME | typeof WORKPLACE_SEARCH_PLUGIN.NAME; export interface Invitation { email: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index fa3b6b56a18b9..9cc1cede549b6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -31,8 +31,8 @@ import { RedirectAppLinks } from '../../../../../../../../../../../src/plugins/k import type { TimeRange } from '../../../../../../../../../../../src/plugins/data/public'; import { esKuery } from '../../../../../../../../../../../src/plugins/data/public'; import { LogStream } from '../../../../../../../../../infra/public'; -import type { Agent } from '../../../../../types'; -import { useStartServices } from '../../../../../hooks'; +import type { Agent, AgentPolicy } from '../../../../../types'; +import { useLink, useStartServices } from '../../../../../hooks'; import { DEFAULT_DATE_RANGE } from './constants'; import { DatasetFilter } from './filter_dataset'; @@ -51,6 +51,7 @@ const DatePickerFlexItem = styled(EuiFlexItem)` export interface AgentLogsProps { agent: Agent; + agentPolicy?: AgentPolicy; state: AgentLogsState; } @@ -64,251 +65,298 @@ export interface AgentLogsState { export const AgentLogsUrlStateHelper = createStateContainerReactHelpers(); -export const AgentLogsUI: React.FunctionComponent = memo(({ agent, state }) => { - const { data, application, http } = useStartServices(); - const { update: updateState } = AgentLogsUrlStateHelper.useTransitions(); +const AgentPolicyLogsNotEnabledCallout: React.FunctionComponent<{ agentPolicy: AgentPolicy }> = ({ + agentPolicy, +}) => { + const { getHref } = useLink(); - // Util to convert date expressions (returned by datepicker) to timestamps (used by LogStream) - const getDateRangeTimestamps = useCallback( - (timeRange: TimeRange) => { - const { min, max } = data.query.timefilter.timefilter.calculateBounds(timeRange); - return min && max - ? { - start: min.valueOf(), - end: max.valueOf(), - } - : undefined; - }, - [data.query.timefilter.timefilter] + return ( + + + } + > + + + + ), + }} + /> + + ); +}; - const tryUpdateDateRange = useCallback( - (timeRange: TimeRange) => { - const timestamps = getDateRangeTimestamps(timeRange); - if (timestamps) { - updateState({ - start: timeRange.from, - end: timeRange.to, - }); - } - }, - [getDateRangeTimestamps, updateState] - ); +export const AgentLogsUI: React.FunctionComponent = memo( + ({ agent, agentPolicy, state }) => { + const { data, application, http } = useStartServices(); + const { update: updateState } = AgentLogsUrlStateHelper.useTransitions(); + + // Util to convert date expressions (returned by datepicker) to timestamps (used by LogStream) + const getDateRangeTimestamps = useCallback( + (timeRange: TimeRange) => { + const { min, max } = data.query.timefilter.timefilter.calculateBounds(timeRange); + return min && max + ? { + start: min.valueOf(), + end: max.valueOf(), + } + : undefined; + }, + [data.query.timefilter.timefilter] + ); + + const tryUpdateDateRange = useCallback( + (timeRange: TimeRange) => { + const timestamps = getDateRangeTimestamps(timeRange); + if (timestamps) { + updateState({ + start: timeRange.from, + end: timeRange.to, + }); + } + }, + [getDateRangeTimestamps, updateState] + ); - const [dateRangeTimestamps, setDateRangeTimestamps] = useState<{ start: number; end: number }>( - getDateRangeTimestamps({ - from: state.start, - to: state.end, - }) || + const [dateRangeTimestamps, setDateRangeTimestamps] = useState<{ start: number; end: number }>( getDateRangeTimestamps({ - from: DEFAULT_DATE_RANGE.start, - to: DEFAULT_DATE_RANGE.end, - })! - ); + from: state.start, + to: state.end, + }) || + getDateRangeTimestamps({ + from: DEFAULT_DATE_RANGE.start, + to: DEFAULT_DATE_RANGE.end, + })! + ); - // Attempts to parse for timestamps when start/end date expressions change - // If invalid date expressions, set expressions back to default - // Otherwise set the new timestamps - useEffect(() => { - const timestampsFromDateRange = getDateRangeTimestamps({ - from: state.start, - to: state.end, - }); - if (!timestampsFromDateRange) { - tryUpdateDateRange({ - from: DEFAULT_DATE_RANGE.start, - to: DEFAULT_DATE_RANGE.end, + // Attempts to parse for timestamps when start/end date expressions change + // If invalid date expressions, set expressions back to default + // Otherwise set the new timestamps + useEffect(() => { + const timestampsFromDateRange = getDateRangeTimestamps({ + from: state.start, + to: state.end, }); - } else { - setDateRangeTimestamps(timestampsFromDateRange); - } - }, [state.start, state.end, getDateRangeTimestamps, tryUpdateDateRange]); + if (!timestampsFromDateRange) { + tryUpdateDateRange({ + from: DEFAULT_DATE_RANGE.start, + to: DEFAULT_DATE_RANGE.end, + }); + } else { + setDateRangeTimestamps(timestampsFromDateRange); + } + }, [state.start, state.end, getDateRangeTimestamps, tryUpdateDateRange]); - // Query validation helper - const isQueryValid = useCallback((testQuery: string) => { - try { - esKuery.fromKueryExpression(testQuery); - return true; - } catch (err) { - return false; - } - }, []); + // Query validation helper + const isQueryValid = useCallback((testQuery: string) => { + try { + esKuery.fromKueryExpression(testQuery); + return true; + } catch (err) { + return false; + } + }, []); - // User query state - const [draftQuery, setDraftQuery] = useState(state.query); - const [isDraftQueryValid, setIsDraftQueryValid] = useState(isQueryValid(state.query)); - const onUpdateDraftQuery = useCallback( - (newDraftQuery: string, runQuery?: boolean) => { - setDraftQuery(newDraftQuery); - if (isQueryValid(newDraftQuery)) { - setIsDraftQueryValid(true); - if (runQuery) { - updateState({ query: newDraftQuery }); + // User query state + const [draftQuery, setDraftQuery] = useState(state.query); + const [isDraftQueryValid, setIsDraftQueryValid] = useState(isQueryValid(state.query)); + const onUpdateDraftQuery = useCallback( + (newDraftQuery: string, runQuery?: boolean) => { + setDraftQuery(newDraftQuery); + if (isQueryValid(newDraftQuery)) { + setIsDraftQueryValid(true); + if (runQuery) { + updateState({ query: newDraftQuery }); + } + } else { + setIsDraftQueryValid(false); } - } else { - setIsDraftQueryValid(false); - } - }, - [isQueryValid, updateState] - ); + }, + [isQueryValid, updateState] + ); - // Build final log stream query from agent id, datasets, log levels, and user input - const logStreamQuery = useMemo( - () => - buildQuery({ - agentId: agent.id, - datasets: state.datasets, - logLevels: state.logLevels, - userQuery: state.query, - }), - [agent.id, state.datasets, state.logLevels, state.query] - ); + // Build final log stream query from agent id, datasets, log levels, and user input + const logStreamQuery = useMemo( + () => + buildQuery({ + agentId: agent.id, + datasets: state.datasets, + logLevels: state.logLevels, + userQuery: state.query, + }), + [agent.id, state.datasets, state.logLevels, state.query] + ); - // Generate URL to pass page state to Logs UI - const viewInLogsUrl = useMemo( - () => - http.basePath.prepend( - url.format({ - pathname: '/app/logs/stream', - search: stringify({ - logPosition: encode({ - start: state.start, - end: state.end, - streamLive: false, + // Generate URL to pass page state to Logs UI + const viewInLogsUrl = useMemo( + () => + http.basePath.prepend( + url.format({ + pathname: '/app/logs/stream', + search: stringify({ + logPosition: encode({ + start: state.start, + end: state.end, + streamLive: false, + }), + logFilter: encode({ + expression: logStreamQuery, + kind: 'kuery', + }), }), - logFilter: encode({ - expression: logStreamQuery, - kind: 'kuery', - }), - }), - }) - ), - [http.basePath, state.start, state.end, logStreamQuery] - ); - - const agentVersion = agent.local_metadata?.elastic?.agent?.version; - const isLogFeatureAvailable = useMemo(() => { - if (!agentVersion) { - return false; - } - const agentVersionWithPrerelease = semverCoerce(agentVersion)?.version; - if (!agentVersionWithPrerelease) { - return false; - } - return semverGte(agentVersionWithPrerelease, '7.11.0'); - }, [agentVersion]); + }) + ), + [http.basePath, state.start, state.end, logStreamQuery] + ); - // Set absolute height on logs component (needed to render correctly in Safari) - // based on available height, or 600px, whichever is greater - const [logsPanelRef, { height: measuredlogPanelHeight }] = useMeasure(); - const logPanelHeight = useMemo(() => Math.max(measuredlogPanelHeight, 600), [ - measuredlogPanelHeight, - ]); + const agentVersion = agent.local_metadata?.elastic?.agent?.version; + const isLogFeatureAvailable = useMemo(() => { + if (!agentVersion) { + return false; + } + const agentVersionWithPrerelease = semverCoerce(agentVersion)?.version; + if (!agentVersionWithPrerelease) { + return false; + } + return semverGte(agentVersionWithPrerelease, '7.11.0'); + }, [agentVersion]); - if (!isLogFeatureAvailable) { - return ( - - - - ), - }} - /> - } - /> - ); - } + // Set absolute height on logs component (needed to render correctly in Safari) + // based on available height, or 600px, whichever is greater + const [logsPanelRef, { height: measuredlogPanelHeight }] = useMeasure(); + const logPanelHeight = useMemo(() => Math.max(measuredlogPanelHeight, 600), [ + measuredlogPanelHeight, + ]); - return ( - - - - - + + + ), + }} /> - - - - { - const currentDatasets = [...state.datasets]; - const datasetPosition = currentDatasets.indexOf(dataset); - if (datasetPosition >= 0) { - currentDatasets.splice(datasetPosition, 1); - updateState({ datasets: currentDatasets }); - } else { - updateState({ datasets: [...state.datasets, dataset] }); - } - }} + } + /> + ); + } + + return ( + + {agentPolicy && !agentPolicy.monitoring_enabled?.includes('logs') && ( + + )} + + + + - { - const currentLevels = [...state.logLevels]; - const levelPosition = currentLevels.indexOf(level); - if (levelPosition >= 0) { - currentLevels.splice(levelPosition, 1); - updateState({ logLevels: currentLevels }); - } else { - updateState({ logLevels: [...state.logLevels, level] }); - } + + + + { + const currentDatasets = [...state.datasets]; + const datasetPosition = currentDatasets.indexOf(dataset); + if (datasetPosition >= 0) { + currentDatasets.splice(datasetPosition, 1); + updateState({ datasets: currentDatasets }); + } else { + updateState({ datasets: [...state.datasets, dataset] }); + } + }} + /> + { + const currentLevels = [...state.logLevels]; + const levelPosition = currentLevels.indexOf(level); + if (levelPosition >= 0) { + currentLevels.splice(levelPosition, 1); + updateState({ logLevels: currentLevels }); + } else { + updateState({ logLevels: [...state.logLevels, level] }); + } + }} + /> + + + + { + tryUpdateDateRange({ + from: start, + to: end, + }); }} /> - - - - { - tryUpdateDateRange({ - from: start, - to: end, - }); - }} + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - ); -}); + + + + + + + ); + } +); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx index ff31ffef03925..0e2c01f095f3e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx @@ -22,57 +22,57 @@ import { DEFAULT_LOGS_STATE, STATE_STORAGE_KEY } from './constants'; import type { AgentLogsProps, AgentLogsState } from './agent_logs'; import { AgentLogsUI, AgentLogsUrlStateHelper } from './agent_logs'; -export const AgentLogs: React.FunctionComponent> = memo( - ({ agent }) => { - const stateContainer = useMemo( - () => - createStateContainer< - AgentLogsState, - { - update: PureTransition]>; - } - >( - { - ...DEFAULT_LOGS_STATE, - ...getStateFromKbnUrl(STATE_STORAGE_KEY, window.location.href), - }, - { - update: (state) => (updatedState) => ({ ...state, ...updatedState }), - } - ), - [] - ); +export const AgentLogs: React.FunctionComponent< + Pick +> = memo(({ agent, agentPolicy }) => { + const stateContainer = useMemo( + () => + createStateContainer< + AgentLogsState, + { + update: PureTransition]>; + } + >( + { + ...DEFAULT_LOGS_STATE, + ...getStateFromKbnUrl(STATE_STORAGE_KEY, window.location.href), + }, + { + update: (state) => (updatedState) => ({ ...state, ...updatedState }), + } + ), + [] + ); - const AgentLogsConnected = useMemo( - () => - AgentLogsUrlStateHelper.connect((state) => ({ - state: state || DEFAULT_LOGS_STATE, - }))(AgentLogsUI), - [] - ); + const AgentLogsConnected = useMemo( + () => + AgentLogsUrlStateHelper.connect((state) => ({ + state: state || DEFAULT_LOGS_STATE, + }))(AgentLogsUI), + [] + ); - const [isSyncReady, setIsSyncReady] = useState(false); + const [isSyncReady, setIsSyncReady] = useState(false); - useEffect(() => { - const stateStorage = createKbnUrlStateStorage(); - const { start, stop } = syncState({ - storageKey: STATE_STORAGE_KEY, - stateContainer: stateContainer as INullableBaseStateContainer, - stateStorage, - }); - start(); - setIsSyncReady(true); + useEffect(() => { + const stateStorage = createKbnUrlStateStorage(); + const { start, stop } = syncState({ + storageKey: STATE_STORAGE_KEY, + stateContainer: stateContainer as INullableBaseStateContainer, + stateStorage, + }); + start(); + setIsSyncReady(true); - return () => { - stop(); - stateContainer.set(DEFAULT_LOGS_STATE); - }; - }, [stateContainer]); + return () => { + stop(); + stateContainer.set(DEFAULT_LOGS_STATE); + }; + }, [stateContainer]); - return ( - - {isSyncReady ? : null} - - ); - } -); + return ( + + {isSyncReady ? : null} + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx index ce213808563e6..559cefc5fc720 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx @@ -306,7 +306,7 @@ const AgentDetailsPageContent: React.FunctionComponent<{ { - return ; + return ; }} /> (); const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; - const history = useHistory(); - const { pathname, search, hash } = useLocation(); + const { search } = useLocation(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); const integration = useMemo(() => queryParams.get('integration'), [queryParams]); const services = useStartServices(); @@ -212,66 +202,19 @@ export function Detail() { (ev) => { ev.preventDefault(); - // The object below, given to `createHref` is explicitly accessing keys of `location` in order - // to ensure that dependencies to this `useCallback` is set correctly (because `location` is mutable) - const currentPath = history.createHref({ - pathname, - search, - hash, - }); - const path = pagePathGetters.add_integration_to_policy({ pkgkey, ...(integration ? { integration } : {}), ...(agentPolicyIdFromContext ? { agentPolicyId: agentPolicyIdFromContext } : {}), })[1]; - let redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] & - CreatePackagePolicyRouteState['onCancelNavigateTo']; - - if (agentPolicyIdFromContext) { - redirectToPath = [ - PLUGIN_ID, - { - path: `#${ - pagePathGetters.policy_details({ - policyId: agentPolicyIdFromContext, - })[1] - }`, - }, - ]; - } else { - redirectToPath = [ - INTEGRATIONS_PLUGIN_ID, - { - path: currentPath, - }, - ]; - } - - const redirectBackRouteState: CreatePackagePolicyRouteState = { - onSaveNavigateTo: redirectToPath, - onCancelNavigateTo: redirectToPath, - onCancelUrl: currentPath, - }; - services.application.navigateToApp(PLUGIN_ID, { // Necessary because of Fleet's HashRouter. Can be changed when // https://github.com/elastic/kibana/issues/96134 is resolved path: `#${path}`, - state: redirectBackRouteState, }); }, - [ - history, - hash, - pathname, - search, - pkgkey, - integration, - services.application, - agentPolicyIdFromContext, - ] + [pkgkey, integration, services.application, agentPolicyIdFromContext] ); const headerRightContent = useMemo( diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts index 5ff233b9e11e1..a527a3c864543 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts @@ -6,7 +6,7 @@ */ import { PaletteOutput } from 'src/plugins/charts/public'; -import { DataType } from '../types'; +import { DataType, SuggestionRequest } from '../types'; import { suggestions } from './suggestions'; import { PieVisualizationState } from './types'; @@ -354,6 +354,81 @@ describe('suggestions', () => { ); }); + it('should score higher for more groups', () => { + const config: SuggestionRequest = { + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'a', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'b', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'e', + operation: { label: 'Count', dataType: 'number' as DataType, isBucketed: false }, + }, + ], + changeType: 'initial', + }, + state: undefined, + keptLayerIds: ['first'], + }; + const twoGroupsResults = suggestions(config); + config.table.columns.splice(1, 1); + const oneGroupResults = suggestions(config); + + expect(Math.max(...twoGroupsResults.map((suggestion) => suggestion.score))).toBeGreaterThan( + Math.max(...oneGroupResults.map((suggestion) => suggestion.score)) + ); + }); + + it('should score higher for more groups for each subvis with passed-in subvis id', () => { + const config: SuggestionRequest = { + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'a', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'b', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'e', + operation: { label: 'Count', dataType: 'number' as DataType, isBucketed: false }, + }, + ], + changeType: 'initial', + }, + state: undefined, + keptLayerIds: ['first'], + subVisualizationId: 'donut', + }; + const twoGroupsResults = suggestions(config); + config.table.columns.splice(1, 1); + const oneGroupResults = suggestions(config); + // collect scores for one or two groups for each sub vis + const scores: Record = {}; + twoGroupsResults.forEach((r) => { + scores[r.state.shape] = { ...(scores[r.state.shape] || {}), two: r.score }; + }); + oneGroupResults.forEach((r) => { + scores[r.state.shape] = { ...(scores[r.state.shape] || {}), one: r.score }; + }); + expect(Object.keys(scores).length).toEqual(2); + Object.values(scores).forEach(({ one, two }) => { + expect(two).toBeGreaterThan(one); + }); + }); + it('should keep passed in palette', () => { const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' }; const results = suggestions({ diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts index 7240203267d7c..644f0a0cd8aaf 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts @@ -51,9 +51,10 @@ export function suggestions({ const results: Array> = []; - if (groups.length <= MAX_PIE_BUCKETS) { - let newShape: PieVisualizationState['shape'] = 'donut'; - if (groups.length !== 1) { + if (groups.length <= MAX_PIE_BUCKETS && subVisualizationId !== 'treemap') { + let newShape: PieVisualizationState['shape'] = + (subVisualizationId as PieVisualizationState['shape']) || 'donut'; + if (groups.length !== 1 && !subVisualizationId) { newShape = 'pie'; } @@ -108,7 +109,10 @@ export function suggestions({ }); } - if (groups.length <= MAX_TREEMAP_BUCKETS) { + if ( + groups.length <= MAX_TREEMAP_BUCKETS && + (!subVisualizationId || subVisualizationId === 'treemap') + ) { results.push({ title: i18n.translate('xpack.lens.pie.treemapSuggestionLabel', { defaultMessage: 'As Treemap', @@ -149,7 +153,11 @@ export function suggestions({ } return [...results] - .sort((a, b) => a.score - b.score) + .map((suggestion) => ({ + ...suggestion, + score: suggestion.score + 0.05 * groups.length, + })) + .sort((a, b) => b.score - a.score) .map((suggestion) => ({ ...suggestion, hide: incompleteConfiguration || suggestion.hide, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts deleted file mode 100644 index 7c3abba3e5b05..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts +++ /dev/null @@ -1,53 +0,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 { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels } from '../constants'; -import { buildPhraseFilter } from '../utils'; -import { TRANSACTION_DURATION } from '../constants/elasticsearch_fieldnames'; - -export function getServiceLatencyLensConfig({ indexPattern }: ConfigProps): DataSeries { - return { - reportType: 'kpi-over-time', - defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'average', - sourceField: 'transaction.duration.us', - label: 'Latency', - }, - ], - hasOperationType: true, - defaultFilters: [ - 'user_agent.name', - 'user_agent.os.name', - 'client.geo.country_name', - 'user_agent.device.name', - ], - breakdowns: [ - 'user_agent.name', - 'user_agent.os.name', - 'client.geo.country_name', - 'user_agent.device.name', - ], - filters: buildPhraseFilter('transaction.type', 'request', indexPattern), - labels: { ...FieldLabels, [TRANSACTION_DURATION]: 'Latency' }, - reportDefinitions: [ - { - field: 'service.name', - required: true, - }, - { - field: 'service.environment', - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index 01e8d023ae96b..52faa2dccaeac 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -96,3 +96,5 @@ export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; export const TERMS_COLUMN = 'TERMS_COLUMN'; export const OPERATION_COLUMN = 'operation'; + +export const REPORT_METRIC_FIELD = 'REPORT_METRIC_FIELD'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts index b5a5169216b7b..6f990015fbc62 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts @@ -13,4 +13,5 @@ export enum URL_KEYS { BREAK_DOWN = 'bd', FILTERS = 'ft', REPORT_DEFINITIONS = 'rdf', + SELECTED_METRIC = 'mt', } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 5189a529bda8f..72b4bd7919c3e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -9,8 +9,14 @@ import { LayerConfig, LensAttributes } from './lens_attributes'; import { mockAppIndexPattern, mockIndexPattern } from '../rtl_helpers'; import { getDefaultConfigs } from './default_configs'; import { sampleAttribute } from './test_data/sample_attribute'; -import { LCP_FIELD, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; +import { + LCP_FIELD, + TRANSACTION_DURATION, + USER_AGENT_NAME, +} from './constants/elasticsearch_fieldnames'; import { buildExistsFilter, buildPhrasesFilter } from './utils'; +import { sampleAttributeKpi } from './test_data/sample_attribute_kpi'; +import { REPORT_METRIC_FIELD } from './constants'; describe('Lens Attribute', () => { mockAppIndexPattern(); @@ -21,12 +27,12 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, }); - reportViewConfig.filters?.push(...buildExistsFilter('transaction.type', mockIndexPattern)); + reportViewConfig.baseFilters?.push(...buildExistsFilter('transaction.type', mockIndexPattern)); let lnsAttr: LensAttributes; const layerConfig: LayerConfig = { - reportConfig: reportViewConfig, + seriesConfig: reportViewConfig, seriesType: 'line', operationType: 'count', indexPattern: mockIndexPattern, @@ -42,6 +48,27 @@ describe('Lens Attribute', () => { expect(lnsAttr.getJSON()).toEqual(sampleAttribute); }); + it('should return expected json for kpi report type', function () { + const seriesConfigKpi = getDefaultConfigs({ + reportType: 'kpi-over-time', + dataType: 'ux', + indexPattern: mockIndexPattern, + }); + + const lnsAttrKpi = new LensAttributes([ + { + seriesConfig: seriesConfigKpi, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'service.name': ['elastic-co'] }, + time: { from: 'now-15m', to: 'now' }, + }, + ]); + + expect(lnsAttrKpi.getJSON()).toEqual(sampleAttributeKpi); + }); + it('should return main y axis', function () { expect(lnsAttr.getMainYAxis(layerConfig)).toEqual({ dataType: 'number', @@ -72,7 +99,7 @@ describe('Lens Attribute', () => { }); it('should return expected field type for custom field with default value', function () { - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta(REPORT_METRIC_FIELD, layerConfig))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -92,7 +119,7 @@ describe('Lens Attribute', () => { it('should return expected field type for custom field with passed value', function () { const layerConfig1: LayerConfig = { - reportConfig: reportViewConfig, + seriesConfig: reportViewConfig, seriesType: 'line', operationType: 'count', indexPattern: mockIndexPattern, @@ -102,20 +129,20 @@ describe('Lens Attribute', () => { lnsAttr = new LensAttributes([layerConfig1]); - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig1))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta(REPORT_METRIC_FIELD, layerConfig1))).toEqual( JSON.stringify({ fieldMeta: { count: 0, - name: LCP_FIELD, + name: TRANSACTION_DURATION, type: 'number', - esTypes: ['scaled_float'], + esTypes: ['long'], scripted: false, searchable: true, aggregatable: true, readFromDocValues: true, }, - fieldName: LCP_FIELD, - columnLabel: 'Largest contentful paint', + fieldName: TRANSACTION_DURATION, + columnLabel: 'Page load time', }) ); }); @@ -269,7 +296,7 @@ describe('Lens Attribute', () => { describe('Layer breakdowns', function () { it('should return breakdown column', function () { const layerConfig1: LayerConfig = { - reportConfig: reportViewConfig, + seriesConfig: reportViewConfig, seriesType: 'line', operationType: 'count', indexPattern: mockIndexPattern, @@ -322,7 +349,7 @@ describe('Lens Attribute', () => { 'x-axis-column-layer0': { dataType: 'number', isBucketed: true, - label: 'Largest contentful paint', + label: 'Page load time', operationType: 'range', params: { maxBars: 'auto', @@ -330,7 +357,7 @@ describe('Lens Attribute', () => { type: 'histogram', }, scale: 'interval', - sourceField: 'transaction.marks.agent.largestContentfulPaint', + sourceField: 'transaction.duration.us', }, 'y-axis-column-layer0': { dataType: 'number', @@ -353,12 +380,12 @@ describe('Lens Attribute', () => { describe('Layer Filters', function () { it('should return expected filters', function () { - reportViewConfig.filters?.push( + reportViewConfig.baseFilters?.push( ...buildPhrasesFilter('service.name', ['elastic', 'kibana'], mockIndexPattern) ); const layerConfig1: LayerConfig = { - reportConfig: reportViewConfig, + seriesConfig: reportViewConfig, seriesType: 'line', operationType: 'count', indexPattern: mockIndexPattern, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 208e8d8ba43c2..eaf9c1c884a9d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -29,8 +29,14 @@ import { } from '../../../../../../lens/public'; import { urlFiltersToKueryString } from '../utils/stringify_kueries'; import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants'; -import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types'; +import { + FieldLabels, + FILTER_RECORDS, + USE_BREAK_DOWN_COLUMN, + TERMS_COLUMN, + REPORT_METRIC_FIELD, +} from './constants'; +import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; import { PersistableFilter } from '../../../../../../lens/common'; import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; @@ -47,54 +53,47 @@ function buildNumberColumn(sourceField: string) { }; } -export const parseCustomFieldName = ( - sourceField: string, - reportViewConfig: DataSeries, - selectedDefinitions: URLReportDefinition -) => { - let fieldName = sourceField; +export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricField?: string) => { let columnType; let columnFilters; let timeScale; let columnLabel; - const rdf = reportViewConfig.reportDefinitions ?? []; - - const customField = rdf.find(({ field }) => field === fieldName); - - if (customField) { - if (selectedDefinitions[fieldName]) { - fieldName = selectedDefinitions[fieldName][0]; - if (customField?.options) { - const currField = customField?.options?.find( - ({ field, id }) => field === fieldName || id === fieldName - ); - columnType = currField?.columnType; - columnFilters = currField?.columnFilters; - timeScale = currField?.timeScale; - columnLabel = currField?.label; - } - } else if (customField.options?.[0].field || customField.options?.[0].id) { - fieldName = customField.options?.[0].field || customField.options?.[0].id; - columnType = customField.options?.[0].columnType; - columnFilters = customField.options?.[0].columnFilters; - timeScale = customField.options?.[0].timeScale; - columnLabel = customField.options?.[0].label; + const metricOptions = seriesConfig.metricOptions ?? []; + + if (selectedMetricField) { + if (metricOptions) { + const currField = metricOptions.find( + ({ field, id }) => field === selectedMetricField || id === selectedMetricField + ); + columnType = currField?.columnType; + columnFilters = currField?.columnFilters; + timeScale = currField?.timeScale; + columnLabel = currField?.label; } + } else if (metricOptions?.[0].field || metricOptions?.[0].id) { + const firstMetricOption = metricOptions?.[0]; + + selectedMetricField = firstMetricOption.field || firstMetricOption.id; + columnType = firstMetricOption.columnType; + columnFilters = firstMetricOption.columnFilters; + timeScale = firstMetricOption.timeScale; + columnLabel = firstMetricOption.label; } - return { fieldName, columnType, columnFilters, timeScale, columnLabel }; + return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel }; }; export interface LayerConfig { filters?: UrlFilter[]; - reportConfig: DataSeries; + seriesConfig: SeriesConfig; breakdown?: string; seriesType?: SeriesType; operationType?: OperationType; reportDefinitions: URLReportDefinition; time: { to: string; from: string }; indexPattern: IndexPattern; + selectedMetricField?: string; } export class LensAttributes { @@ -105,9 +104,9 @@ export class LensAttributes { constructor(layerConfigs: LayerConfig[]) { this.layers = {}; - layerConfigs.forEach(({ reportConfig, operationType }) => { + layerConfigs.forEach(({ seriesConfig, operationType }) => { if (operationType) { - reportConfig.yAxisColumns.forEach((yAxisColumn) => { + seriesConfig.yAxisColumns.forEach((yAxisColumn) => { if (typeof yAxisColumn.operationType !== undefined) { yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; } @@ -150,12 +149,12 @@ export class LensAttributes { getNumberRangeColumn( sourceField: string, - reportViewConfig: DataSeries, + seriesConfig: SeriesConfig, label?: string ): RangeIndexPatternColumn { return { sourceField, - label: reportViewConfig.labels[sourceField] ?? label, + label: seriesConfig.labels[sourceField] ?? label, dataType: 'number', operationType: 'range', isBucketed: true, @@ -171,22 +170,22 @@ export class LensAttributes { getCardinalityColumn({ sourceField, label, - reportViewConfig, + seriesConfig, }: { sourceField: string; label?: string; - reportViewConfig: DataSeries; + seriesConfig: SeriesConfig; }) { return this.getNumberOperationColumn({ sourceField, operationType: 'unique_count', label, - reportViewConfig, + seriesConfig, }); } getNumberColumn({ - reportViewConfig, + seriesConfig, label, sourceField, columnType, @@ -196,7 +195,7 @@ export class LensAttributes { columnType?: string; operationType?: string; label?: string; - reportViewConfig: DataSeries; + seriesConfig: SeriesConfig; }) { if (columnType === 'operation' || operationType) { if ( @@ -209,26 +208,26 @@ export class LensAttributes { sourceField, operationType, label, - reportViewConfig, + seriesConfig, }); } if (operationType?.includes('th')) { - return this.getPercentileNumberColumn(sourceField, operationType, reportViewConfig!); + return this.getPercentileNumberColumn(sourceField, operationType, seriesConfig!); } } - return this.getNumberRangeColumn(sourceField, reportViewConfig!, label); + return this.getNumberRangeColumn(sourceField, seriesConfig!, label); } getNumberOperationColumn({ sourceField, label, - reportViewConfig, + seriesConfig, operationType, }: { sourceField: string; operationType: 'average' | 'median' | 'sum' | 'unique_count'; label?: string; - reportViewConfig: DataSeries; + seriesConfig: SeriesConfig; }): | AvgIndexPatternColumn | MedianIndexPatternColumn @@ -239,7 +238,7 @@ export class LensAttributes { label: i18n.translate('xpack.observability.expView.columns.operation.label', { defaultMessage: '{operationType} of {sourceField}', values: { - sourceField: label || reportViewConfig.labels[sourceField], + sourceField: label || seriesConfig.labels[sourceField], operationType: capitalize(operationType), }, }), @@ -250,13 +249,13 @@ export class LensAttributes { getPercentileNumberColumn( sourceField: string, percentileValue: string, - reportViewConfig: DataSeries + seriesConfig: SeriesConfig ): PercentileIndexPatternColumn { return { ...buildNumberColumn(sourceField), label: i18n.translate('xpack.observability.expView.columns.label', { defaultMessage: '{percentileValue} percentile of {sourceField}', - values: { sourceField: reportViewConfig.labels[sourceField], percentileValue }, + values: { sourceField: seriesConfig.labels[sourceField], percentileValue }, }), operationType: 'percentile', params: { percentile: Number(percentileValue.split('th')[0]) }, @@ -295,13 +294,13 @@ export class LensAttributes { } getXAxis(layerConfig: LayerConfig, layerId: string) { - const { xAxisColumn } = layerConfig.reportConfig; + const { xAxisColumn } = layerConfig.seriesConfig; if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { return this.getBreakdownColumn({ layerId, indexPattern: layerConfig.indexPattern, - sourceField: layerConfig.breakdown || layerConfig.reportConfig.breakdowns[0], + sourceField: layerConfig.breakdown || layerConfig.seriesConfig.breakdownFields[0], }); } @@ -333,6 +332,7 @@ export class LensAttributes { timeScale, columnFilters, } = this.getFieldMeta(sourceField, layerConfig); + const { type: fieldType } = fieldMeta ?? {}; if (columnType === TERMS_COLUMN) { @@ -356,14 +356,14 @@ export class LensAttributes { columnType, operationType, label: columnLabel || label, - reportViewConfig: layerConfig.reportConfig, + seriesConfig: layerConfig.seriesConfig, }); } if (operationType === 'unique_count') { return this.getCardinalityColumn({ sourceField: fieldName, label: columnLabel || label, - reportViewConfig: layerConfig.reportConfig, + seriesConfig: layerConfig.seriesConfig, }); } @@ -378,32 +378,26 @@ export class LensAttributes { sourceField: string; layerConfig: LayerConfig; }) { - return parseCustomFieldName( - sourceField, - layerConfig.reportConfig, - layerConfig.reportDefinitions - ); + return parseCustomFieldName(layerConfig.seriesConfig, sourceField); } getFieldMeta(sourceField: string, layerConfig: LayerConfig) { - const { - fieldName, - columnType, - columnLabel, - columnFilters, - timeScale, - } = this.getCustomFieldName({ - sourceField, - layerConfig, - }); - - const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName); + if (sourceField === REPORT_METRIC_FIELD) { + const { fieldName, columnType, columnLabel, columnFilters, timeScale } = parseCustomFieldName( + layerConfig.seriesConfig, + layerConfig.selectedMetricField + ); + const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName!); + return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale }; + } else { + const fieldMeta = layerConfig.indexPattern.getFieldByName(sourceField); - return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale }; + return { fieldMeta, fieldName: sourceField }; + } } getMainYAxis(layerConfig: LayerConfig) { - const { sourceField, operationType, label } = layerConfig.reportConfig.yAxisColumns[0]; + const { sourceField, operationType, label } = layerConfig.seriesConfig.yAxisColumns[0]; if (sourceField === 'Records' || !sourceField) { return this.getRecordsColumn(label); @@ -420,7 +414,7 @@ export class LensAttributes { getChildYAxises(layerConfig: LayerConfig) { const lensColumns: Record = {}; - const yAxisColumns = layerConfig.reportConfig.yAxisColumns; + const yAxisColumns = layerConfig.seriesConfig.yAxisColumns; // 1 means there is only main y axis if (yAxisColumns.length === 1) { return lensColumns; @@ -460,7 +454,7 @@ export class LensAttributes { const { filters, time: { from, to }, - reportConfig: { filters: layerFilters, reportType }, + seriesConfig: { baseFilters: layerFilters, reportType }, } = layerConfig; let baseFilters = ''; if (reportType !== 'kpi-over-time' && totalLayers > 1) { @@ -522,7 +516,7 @@ export class LensAttributes { } getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { - if (index === 0 || mainLayerConfig.reportConfig.reportType !== 'kpi-over-time') { + if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') { return null; } @@ -603,16 +597,16 @@ export class LensAttributes { ...Object.keys(this.getChildYAxises(layerConfig)), ], layerId: `layer${index}`, - seriesType: layerConfig.seriesType || layerConfig.reportConfig.defaultSeriesType, - palette: layerConfig.reportConfig.palette, - yConfig: layerConfig.reportConfig.yConfig || [ + seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType, + palette: layerConfig.seriesConfig.palette, + yConfig: layerConfig.seriesConfig.yConfig || [ { forAccessor: `y-axis-column-layer${index}` }, ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}), })), - ...(this.layerConfigs[0].reportConfig.yTitle - ? { yTitle: this.layerConfigs[0].reportConfig.yTitle } + ...(this.layerConfigs[0].seriesConfig.yTitle + ? { yTitle: this.layerConfigs[0].seriesConfig.yTitle } : {}), }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts deleted file mode 100644 index 2d44e122af82d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataSeries, ConfigProps } from '../../types'; -import { FieldLabels } from '../constants'; - -export function getCPUUsageLensConfig({}: ConfigProps): DataSeries { - return { - reportType: 'kpi-over-time', - defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'average', - sourceField: 'system.cpu.user.pct', - label: 'CPU Usage %', - }, - ], - hasOperationType: true, - defaultFilters: [], - breakdowns: ['host.hostname'], - filters: [], - labels: { ...FieldLabels, 'host.hostname': 'Host name' }, - reportDefinitions: [ - { - field: 'agent.hostname', - required: true, - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts deleted file mode 100644 index deaa551dce657..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataSeries, ConfigProps } from '../../types'; -import { FieldLabels } from '../constants'; - -export function getMemoryUsageLensConfig({}: ConfigProps): DataSeries { - return { - reportType: 'kpi-over-time', - defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'average', - sourceField: 'system.memory.used.pct', - label: 'Memory Usage %', - }, - ], - hasOperationType: true, - defaultFilters: [], - breakdowns: ['host.hostname'], - filters: [], - labels: { ...FieldLabels, 'host.hostname': 'Host name' }, - reportDefinitions: [ - { - field: 'host.hostname', - required: true, - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts deleted file mode 100644 index d27cdba207d63..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataSeries, ConfigProps } from '../../types'; -import { FieldLabels } from '../constants'; - -export function getNetworkActivityLensConfig({}: ConfigProps): DataSeries { - return { - reportType: 'kpi-over-time', - defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'average', - sourceField: 'system.memory.used.pct', - }, - ], - hasOperationType: true, - defaultFilters: [], - breakdowns: ['host.hostname'], - filters: [], - labels: { ...FieldLabels, 'host.hostname': 'Host name' }, - reportDefinitions: [ - { - field: 'host.hostname', - required: true, - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index e1cb5a0370fb2..98979b9922a86 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; +import { ConfigProps, SeriesConfig } from '../../types'; import { FieldLabels, USE_BREAK_DOWN_COLUMN } from '../constants'; import { buildPhraseFilter } from '../utils'; import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; import { MobileFields } from './mobile_fields'; -export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries { +export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: 'device-data-distribution', defaultSeriesType: 'bar', @@ -28,9 +28,9 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) }, ], hasOperationType: false, - defaultFilters: Object.keys(MobileFields), - breakdowns: Object.keys(MobileFields), - filters: [ + filterFields: Object.keys(MobileFields), + breakdownFields: Object.keys(MobileFields), + baseFilters: [ ...buildPhraseFilter('agent.name', 'iOS/swift', indexPattern), ...buildPhraseFilter('processor.event', 'transaction', indexPattern), ], @@ -39,11 +39,6 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, - reportDefinitions: [ - { - field: SERVICE_NAME, - required: true, - }, - ], + definitionFields: [SERVICE_NAME], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts index 62dd38e55a32a..b9894347d96c0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -19,13 +19,13 @@ import { import { CPU_USAGE, MEMORY_USAGE, MOBILE_APP, RESPONSE_LATENCY } from '../constants/labels'; import { MobileFields } from './mobile_fields'; -export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): DataSeries { +export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: 'data-distribution', defaultSeriesType: 'bar', seriesTypes: ['line', 'bar'], xAxisColumn: { - sourceField: 'performance.metric', + sourceField: REPORT_METRIC_FIELD, }, yAxisColumns: [ { @@ -33,9 +33,9 @@ export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): D }, ], hasOperationType: false, - defaultFilters: Object.keys(MobileFields), - breakdowns: Object.keys(MobileFields), - filters: [ + filterFields: Object.keys(MobileFields), + breakdownFields: Object.keys(MobileFields), + baseFilters: [ ...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], indexPattern), ], labels: { @@ -43,38 +43,25 @@ export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): D ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, - reportDefinitions: [ + definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], + metricOptions: [ { - field: SERVICE_NAME, - required: true, + label: RESPONSE_LATENCY, + field: TRANSACTION_DURATION, + id: TRANSACTION_DURATION, + columnType: OPERATION_COLUMN, }, { - field: SERVICE_ENVIRONMENT, - required: true, + label: MEMORY_USAGE, + field: METRIC_SYSTEM_MEMORY_USAGE, + id: METRIC_SYSTEM_MEMORY_USAGE, + columnType: OPERATION_COLUMN, }, { - field: 'performance.metric', - custom: true, - options: [ - { - label: RESPONSE_LATENCY, - field: TRANSACTION_DURATION, - id: TRANSACTION_DURATION, - columnType: OPERATION_COLUMN, - }, - { - label: MEMORY_USAGE, - field: METRIC_SYSTEM_MEMORY_USAGE, - id: METRIC_SYSTEM_MEMORY_USAGE, - columnType: OPERATION_COLUMN, - }, - { - label: CPU_USAGE, - field: METRIC_SYSTEM_CPU_USAGE, - id: METRIC_SYSTEM_CPU_USAGE, - columnType: OPERATION_COLUMN, - }, - ], + label: CPU_USAGE, + field: METRIC_SYSTEM_CPU_USAGE, + id: METRIC_SYSTEM_CPU_USAGE, + columnType: OPERATION_COLUMN, }, ], }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts index 9a2e86a8f7969..945a631078a33 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -24,7 +24,7 @@ import { } from '../constants/labels'; import { MobileFields } from './mobile_fields'; -export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { +export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: 'kpi-over-time', defaultSeriesType: 'line', @@ -34,14 +34,14 @@ export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { }, yAxisColumns: [ { - sourceField: 'business.kpi', + sourceField: REPORT_METRIC_FIELD, operationType: 'median', }, ], hasOperationType: true, - defaultFilters: Object.keys(MobileFields), - breakdowns: Object.keys(MobileFields), - filters: [ + filterFields: Object.keys(MobileFields), + breakdownFields: Object.keys(MobileFields), + baseFilters: [ ...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], indexPattern), ], labels: { @@ -52,50 +52,37 @@ export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { [METRIC_SYSTEM_MEMORY_USAGE]: MEMORY_USAGE, [METRIC_SYSTEM_CPU_USAGE]: CPU_USAGE, }, - reportDefinitions: [ + definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], + metricOptions: [ { - field: SERVICE_NAME, - required: true, + label: RESPONSE_LATENCY, + field: TRANSACTION_DURATION, + id: TRANSACTION_DURATION, + columnType: OPERATION_COLUMN, }, { - field: SERVICE_ENVIRONMENT, - required: true, - }, - { - field: 'business.kpi', - custom: true, - options: [ - { - label: RESPONSE_LATENCY, - field: TRANSACTION_DURATION, - id: TRANSACTION_DURATION, - columnType: OPERATION_COLUMN, - }, - { - field: RECORDS_FIELD, - id: RECORDS_FIELD, - label: TRANSACTIONS_PER_MINUTE, - columnFilters: [ - { - language: 'kuery', - query: `processor.event: transaction`, - }, - ], - timeScale: 'm', - }, + field: RECORDS_FIELD, + id: RECORDS_FIELD, + label: TRANSACTIONS_PER_MINUTE, + columnFilters: [ { - label: MEMORY_USAGE, - field: METRIC_SYSTEM_MEMORY_USAGE, - id: METRIC_SYSTEM_MEMORY_USAGE, - columnType: OPERATION_COLUMN, - }, - { - label: CPU_USAGE, - field: METRIC_SYSTEM_CPU_USAGE, - id: METRIC_SYSTEM_CPU_USAGE, - columnType: OPERATION_COLUMN, + language: 'kuery', + query: `processor.event: transaction`, }, ], + timeScale: 'm', + }, + { + label: MEMORY_USAGE, + field: METRIC_SYSTEM_MEMORY_USAGE, + id: METRIC_SYSTEM_MEMORY_USAGE, + columnType: OPERATION_COLUMN, + }, + { + label: CPU_USAGE, + field: METRIC_SYSTEM_CPU_USAGE, + id: METRIC_SYSTEM_CPU_USAGE, + columnType: OPERATION_COLUMN, }, ], }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index e34d8b0dcfdd0..1d04a9b389503 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -6,8 +6,13 @@ */ import { euiPaletteForStatus } from '@elastic/eui'; -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { + FieldLabels, + FILTER_RECORDS, + REPORT_METRIC_FIELD, + USE_BREAK_DOWN_COLUMN, +} from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -27,7 +32,7 @@ import { SERVICE_ENVIRONMENT, } from '../constants/elasticsearch_fieldnames'; -export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): DataSeries { +export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesConfig { const statusPallete = euiPaletteForStatus(3); return { @@ -39,20 +44,20 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): DataSerie }, yAxisColumns: [ { - sourceField: 'core.web.vitals', + sourceField: REPORT_METRIC_FIELD, label: 'Good', }, { - sourceField: 'core.web.vitals', + sourceField: REPORT_METRIC_FIELD, label: 'Average', }, { - sourceField: 'core.web.vitals', + sourceField: REPORT_METRIC_FIELD, label: 'Poor', }, ], hasOperationType: false, - defaultFilters: [ + filterFields: [ { field: TRANSACTION_URL, isNegated: false, @@ -69,7 +74,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): DataSerie nested: USER_AGENT_VERSION, }, ], - breakdowns: [ + breakdownFields: [ SERVICE_NAME, USER_AGENT_NAME, USER_AGENT_OS, @@ -77,79 +82,67 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): DataSerie USER_AGENT_DEVICE, URL_FULL, ], - filters: [ + baseFilters: [ ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), ], labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application' }, - reportDefinitions: [ + definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], + metricOptions: [ { - field: SERVICE_NAME, - required: true, + id: LCP_FIELD, + label: 'Largest contentful paint', + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: `${LCP_FIELD} < 2500`, + }, + { + language: 'kuery', + query: `${LCP_FIELD} > 2500 and ${LCP_FIELD} < 4000`, + }, + { + language: 'kuery', + query: `${LCP_FIELD} > 4000`, + }, + ], }, { - field: SERVICE_ENVIRONMENT, + label: 'First input delay', + id: FID_FIELD, + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: `${FID_FIELD} < 100`, + }, + { + language: 'kuery', + query: `${FID_FIELD} > 100 and ${FID_FIELD} < 300`, + }, + { + language: 'kuery', + query: `${FID_FIELD} > 300`, + }, + ], }, { - field: 'core.web.vitals', - custom: true, - options: [ + label: 'Cumulative layout shift', + id: CLS_FIELD, + columnType: FILTER_RECORDS, + columnFilters: [ { - id: LCP_FIELD, - label: 'Largest contentful paint', - columnType: FILTER_RECORDS, - columnFilters: [ - { - language: 'kuery', - query: `${LCP_FIELD} < 2500`, - }, - { - language: 'kuery', - query: `${LCP_FIELD} > 2500 and ${LCP_FIELD} < 4000`, - }, - { - language: 'kuery', - query: `${LCP_FIELD} > 4000`, - }, - ], + language: 'kuery', + query: `${CLS_FIELD} < 0.1`, }, { - label: 'First input delay', - id: FID_FIELD, - columnType: FILTER_RECORDS, - columnFilters: [ - { - language: 'kuery', - query: `${FID_FIELD} < 100`, - }, - { - language: 'kuery', - query: `${FID_FIELD} > 100 and ${FID_FIELD} < 300`, - }, - { - language: 'kuery', - query: `${FID_FIELD} > 300`, - }, - ], + language: 'kuery', + query: `${CLS_FIELD} > 0.1 and ${CLS_FIELD} < 0.25`, }, { - label: 'Cumulative layout shift', - id: CLS_FIELD, - columnType: FILTER_RECORDS, - columnFilters: [ - { - language: 'kuery', - query: `${CLS_FIELD} < 0.1`, - }, - { - language: 'kuery', - query: `${CLS_FIELD} > 0.1 and ${CLS_FIELD} < 0.25`, - }, - { - language: 'kuery', - query: `${CLS_FIELD} > 0.25`, - }, - ], + language: 'kuery', + query: `${CLS_FIELD} > 0.25`, }, ], }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts index 812f1b2e4cf33..b171edf2901d5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, RECORDS_FIELD } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -39,13 +39,13 @@ import { WEB_APPLICATION_LABEL, } from '../constants/labels'; -export function getRumDistributionConfig({ indexPattern }: ConfigProps): DataSeries { +export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: 'data-distribution', defaultSeriesType: 'line', seriesTypes: [], xAxisColumn: { - sourceField: 'performance.metric', + sourceField: REPORT_METRIC_FIELD, }, yAxisColumns: [ { @@ -54,7 +54,7 @@ export function getRumDistributionConfig({ indexPattern }: ConfigProps): DataSer }, ], hasOperationType: false, - defaultFilters: [ + filterFields: [ { field: TRANSACTION_URL, isNegated: false, @@ -67,34 +67,22 @@ export function getRumDistributionConfig({ indexPattern }: ConfigProps): DataSer nested: USER_AGENT_VERSION, }, ], - breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], - reportDefinitions: [ + breakdownFields: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], + definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], + metricOptions: [ + { label: PAGE_LOAD_TIME_LABEL, id: TRANSACTION_DURATION, field: TRANSACTION_DURATION }, { - field: SERVICE_NAME, - required: true, - }, - { - field: SERVICE_ENVIRONMENT, - }, - { - field: 'performance.metric', - custom: true, - options: [ - { label: PAGE_LOAD_TIME_LABEL, id: TRANSACTION_DURATION, field: TRANSACTION_DURATION }, - { - label: BACKEND_TIME_LABEL, - id: TRANSACTION_TIME_TO_FIRST_BYTE, - field: TRANSACTION_TIME_TO_FIRST_BYTE, - }, - { label: FCP_LABEL, id: FCP_FIELD, field: FCP_FIELD }, - { label: TBT_LABEL, id: TBT_FIELD, field: TBT_FIELD }, - { label: LCP_LABEL, id: LCP_FIELD, field: LCP_FIELD }, - { label: FID_LABEL, id: FID_FIELD, field: FID_FIELD }, - { label: CLS_LABEL, id: CLS_FIELD, field: CLS_FIELD }, - ], + label: BACKEND_TIME_LABEL, + id: TRANSACTION_TIME_TO_FIRST_BYTE, + field: TRANSACTION_TIME_TO_FIRST_BYTE, }, + { label: FCP_LABEL, id: FCP_FIELD, field: FCP_FIELD }, + { label: TBT_LABEL, id: TBT_FIELD, field: TBT_FIELD }, + { label: LCP_LABEL, id: LCP_FIELD, field: LCP_FIELD }, + { label: FID_LABEL, id: FID_FIELD, field: FID_FIELD }, + { label: CLS_LABEL, id: CLS_FIELD, field: CLS_FIELD }, ], - filters: [ + baseFilters: [ ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), ], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts index 12d66c55c7d00..5899b16d12b4f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -39,7 +39,7 @@ import { WEB_APPLICATION_LABEL, } from '../constants/labels'; -export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): DataSeries { +export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesConfig { return { defaultSeriesType: 'bar_stacked', seriesTypes: [], @@ -49,12 +49,12 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): DataSerie }, yAxisColumns: [ { - sourceField: 'business.kpi', + sourceField: REPORT_METRIC_FIELD, operationType: 'median', }, ], hasOperationType: false, - defaultFilters: [ + filterFields: [ { field: TRANSACTION_URL, isNegated: false, @@ -67,44 +67,32 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): DataSerie nested: USER_AGENT_VERSION, }, ], - breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], - filters: [ + breakdownFields: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], + baseFilters: [ ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), ], labels: { ...FieldLabels, [SERVICE_NAME]: WEB_APPLICATION_LABEL }, - reportDefinitions: [ + definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], + metricOptions: [ + { field: RECORDS_FIELD, id: RECORDS_FIELD, label: PAGE_VIEWS_LABEL }, { - field: SERVICE_NAME, - required: true, + label: PAGE_LOAD_TIME_LABEL, + field: TRANSACTION_DURATION, + id: TRANSACTION_DURATION, + columnType: OPERATION_COLUMN, }, { - field: SERVICE_ENVIRONMENT, - }, - { - field: 'business.kpi', - custom: true, - options: [ - { field: RECORDS_FIELD, id: RECORDS_FIELD, label: PAGE_VIEWS_LABEL }, - { - label: PAGE_LOAD_TIME_LABEL, - field: TRANSACTION_DURATION, - id: TRANSACTION_DURATION, - columnType: OPERATION_COLUMN, - }, - { - label: BACKEND_TIME_LABEL, - field: TRANSACTION_TIME_TO_FIRST_BYTE, - id: TRANSACTION_TIME_TO_FIRST_BYTE, - columnType: OPERATION_COLUMN, - }, - { label: FCP_LABEL, field: FCP_FIELD, id: FCP_FIELD, columnType: OPERATION_COLUMN }, - { label: TBT_LABEL, field: TBT_FIELD, id: TBT_FIELD, columnType: OPERATION_COLUMN }, - { label: LCP_LABEL, field: LCP_FIELD, id: LCP_FIELD, columnType: OPERATION_COLUMN }, - { label: FID_LABEL, field: FID_FIELD, id: FID_FIELD, columnType: OPERATION_COLUMN }, - { label: CLS_LABEL, field: CLS_FIELD, id: CLS_FIELD, columnType: OPERATION_COLUMN }, - ], + label: BACKEND_TIME_LABEL, + field: TRANSACTION_TIME_TO_FIRST_BYTE, + id: TRANSACTION_TIME_TO_FIRST_BYTE, + columnType: OPERATION_COLUMN, }, + { label: FCP_LABEL, field: FCP_FIELD, id: FCP_FIELD, columnType: OPERATION_COLUMN }, + { label: TBT_LABEL, field: TBT_FIELD, id: TBT_FIELD, columnType: OPERATION_COLUMN }, + { label: LCP_LABEL, field: LCP_FIELD, id: LCP_FIELD, columnType: OPERATION_COLUMN }, + { label: FID_LABEL, field: FID_FIELD, id: FID_FIELD, columnType: OPERATION_COLUMN }, + { label: CLS_LABEL, field: CLS_FIELD, id: CLS_FIELD, columnType: OPERATION_COLUMN }, ], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index b958c0dd71528..9783f63f5b901 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -5,18 +5,21 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, RECORDS_FIELD } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildExistsFilter } from '../utils'; import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels'; -export function getSyntheticsDistributionConfig({ series, indexPattern }: ConfigProps): DataSeries { +export function getSyntheticsDistributionConfig({ + series, + indexPattern, +}: ConfigProps): SeriesConfig { return { reportType: 'data-distribution', defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { - sourceField: 'performance.metric', + sourceField: REPORT_METRIC_FIELD, }, yAxisColumns: [ { @@ -25,8 +28,8 @@ export function getSyntheticsDistributionConfig({ series, indexPattern }: Config }, ], hasOperationType: false, - defaultFilters: ['monitor.type', 'observer.geo.name', 'tags'], - breakdowns: [ + filterFields: ['monitor.type', 'observer.geo.name', 'tags'], + breakdownFields: [ 'observer.geo.name', 'monitor.name', 'monitor.id', @@ -34,21 +37,10 @@ export function getSyntheticsDistributionConfig({ series, indexPattern }: Config 'tags', 'url.port', ], - filters: [...buildExistsFilter('summary.up', indexPattern)], - reportDefinitions: [ - { - field: 'monitor.name', - }, - { - field: 'url.full', - }, - { - field: 'performance.metric', - custom: true, - options: [ - { label: 'Monitor duration', id: 'monitor.duration.us', field: 'monitor.duration.us' }, - ], - }, + baseFilters: [...buildExistsFilter('summary.up', indexPattern)], + definitionFields: ['monitor.name', 'url.full'], + metricOptions: [ + { label: 'Monitor duration', id: 'monitor.duration.us', field: 'monitor.duration.us' }, ], labels: { ...FieldLabels, 'monitor.duration.us': MONITORS_DURATION_LABEL }, }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 3e92845436363..6bf280e93eb11 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels, OPERATION_COLUMN } from '../constants'; +import { ConfigProps, SeriesConfig } from '../../types'; +import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants'; import { buildExistsFilter } from '../utils'; import { DOWN_LABEL, MONITORS_DURATION_LABEL, UP_LABEL } from '../constants/labels'; import { MONITOR_DURATION_US } from '../constants/field_names/synthetics'; const SUMMARY_UP = 'summary.up'; const SUMMARY_DOWN = 'summary.down'; -export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): DataSeries { +export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: 'kpi-over-time', defaultSeriesType: 'bar_stacked', @@ -23,45 +23,34 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): DataSerie }, yAxisColumns: [ { - sourceField: 'business.kpi', + sourceField: REPORT_METRIC_FIELD, operationType: 'median', }, ], hasOperationType: false, - defaultFilters: ['observer.geo.name', 'monitor.type', 'tags'], - breakdowns: ['observer.geo.name', 'monitor.type'], - filters: [...buildExistsFilter('summary.up', indexPattern)], + filterFields: ['observer.geo.name', 'monitor.type', 'tags'], + breakdownFields: ['observer.geo.name', 'monitor.type'], + baseFilters: [...buildExistsFilter('summary.up', indexPattern)], palette: { type: 'palette', name: 'status' }, - reportDefinitions: [ + definitionFields: ['monitor.name', 'url.full'], + metricOptions: [ { - field: 'monitor.name', + label: MONITORS_DURATION_LABEL, + field: MONITOR_DURATION_US, + id: MONITOR_DURATION_US, + columnType: OPERATION_COLUMN, }, { - field: 'url.full', + field: SUMMARY_UP, + id: SUMMARY_UP, + label: UP_LABEL, + columnType: OPERATION_COLUMN, }, { - field: 'business.kpi', - custom: true, - options: [ - { - label: MONITORS_DURATION_LABEL, - field: MONITOR_DURATION_US, - id: MONITOR_DURATION_US, - columnType: OPERATION_COLUMN, - }, - { - field: SUMMARY_UP, - id: SUMMARY_UP, - label: UP_LABEL, - columnType: OPERATION_COLUMN, - }, - { - field: SUMMARY_DOWN, - id: SUMMARY_DOWN, - label: DOWN_LABEL, - columnType: OPERATION_COLUMN, - }, - ], + field: SUMMARY_DOWN, + id: SUMMARY_DOWN, + label: DOWN_LABEL, + columnType: OPERATION_COLUMN, }, ], labels: { ...FieldLabels }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts new file mode 100644 index 0000000000000..7f066caf66bf1 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const sampleAttributeKpi = { + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', + references: [ + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer0: { + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], + columns: { + 'x-axis-column-layer0': { + sourceField: '@timestamp', + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + }, + 'y-axis-column-layer0': { + dataType: 'number', + isBucketed: false, + label: 'Page views', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + filter: { + query: 'transaction.type: page-load and processor.event: transaction', + language: 'kuery', + }, + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + fittingFunction: 'Linear', + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', + layers: [ + { + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', + seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], + xAccessor: 'x-axis-column-layer0', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index 9b1e7ec141ca2..f7df2939d9909 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -21,6 +21,7 @@ export function convertToShortUrl(series: SeriesUrl) { filters, reportDefinitions, dataType, + selectedMetricField, ...restSeries } = series; @@ -32,6 +33,7 @@ export function convertToShortUrl(series: SeriesUrl) { [URL_KEYS.FILTERS]: filters, [URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions, [URL_KEYS.DATA_TYPE]: dataType, + [URL_KEYS.SELECTED_METRIC]: selectedMetricField, ...restSeries, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 11487afe28e96..d14a26d13d928 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -12,25 +12,16 @@ import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; import { useSeriesStorage } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { DataSeries, SeriesUrl, UrlFilter } from '../types'; +import { SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; -export const getFiltersFromDefs = ( - reportDefinitions: SeriesUrl['reportDefinitions'], - dataViewConfig: DataSeries -) => { - const rdfFilters = Object.entries(reportDefinitions ?? {}).map(([field, value]) => { +export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => { + return Object.entries(reportDefinitions ?? {}).map(([field, value]) => { return { field, values: value, }; }) as UrlFilter[]; - - // let's filter out custom fields - return rdfFilters.filter(({ field }) => { - const rdf = dataViewConfig.reportDefinitions.find(({ field: fd }) => field === fd); - return !rdf?.custom; - }); }; export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { @@ -49,25 +40,26 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null const seriesT = allSeries[seriesIdT]; const indexPattern = indexPatterns?.[seriesT?.dataType]; if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { - const reportViewConfig = getDefaultConfigs({ + const seriesConfig = getDefaultConfigs({ reportType: seriesT.reportType, dataType: seriesT.dataType, indexPattern, }); const filters: UrlFilter[] = (seriesT.filters ?? []).concat( - getFiltersFromDefs(seriesT.reportDefinitions, reportViewConfig) + getFiltersFromDefs(seriesT.reportDefinitions) ); layerConfigs.push({ filters, indexPattern, - reportConfig: reportViewConfig, + seriesConfig, + time: seriesT.time, breakdown: seriesT.breakdown, - operationType: seriesT.operationType, seriesType: seriesT.seriesType, + operationType: seriesT.operationType, reportDefinitions: seriesT.reportDefinitions ?? {}, - time: seriesT.time, + selectedMetricField: seriesT.selectedMetricField, }); } }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index e9ae43950d47d..7e9b69a276d0b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -110,7 +110,7 @@ export function useSeriesStorage() { } function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { - const { dt, op, st, rt, bd, ft, time, rdf, ...restSeries } = newValue; + const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue; return { operationType: op, reportType: rt!, @@ -120,6 +120,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { time: time!, reportDefinitions: rdf, dataType: dt!, + selectedMetricField: mt, ...restSeries, }; } @@ -132,6 +133,7 @@ interface ShortUrlSeries { [URL_KEYS.BREAK_DOWN]?: string; [URL_KEYS.FILTERS]?: UrlFilter[]; [URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition; + [URL_KEYS.SELECTED_METRIC]?: string; time?: { to: string; from: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx index 203382afc1624..a5e5ad3900ded 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -21,7 +21,7 @@ describe('Series Builder ReportBreakdowns', function () { }); it('should render properly', function () { - render(); + render(); screen.getByText('Select an option: , is selected'); screen.getAllByText('Browser family'); @@ -29,7 +29,7 @@ describe('Series Builder ReportBreakdowns', function () { it('should set new series breakdown on change', function () { const { setSeries } = render( - + ); const btn = screen.getByRole('button', { @@ -51,7 +51,7 @@ describe('Series Builder ReportBreakdowns', function () { }); it('should set undefined on new series on no select breakdown', function () { const { setSeries } = render( - + ); const btn = screen.getByRole('button', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx index e95cd894df5f2..fa2d01691ce1d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -7,19 +7,19 @@ import React from 'react'; import { Breakdowns } from '../../series_editor/columns/breakdowns'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; export function ReportBreakdowns({ seriesId, - dataViewSeries, + seriesConfig, }: { - dataViewSeries: DataSeries; + seriesConfig: SeriesConfig; seriesId: string; }) { return ( ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index 2e5c674b9fad8..cac1eccada311 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -21,7 +21,7 @@ describe('Series Builder ReportDefinitionCol', function () { mockAppIndexPattern(); const seriesId = 'test-series-id'; - const dataViewSeries = getDefaultConfigs({ + const seriesConfig = getDefaultConfigs({ reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', @@ -41,7 +41,7 @@ describe('Series Builder ReportDefinitionCol', function () { mockUseValuesList([{ label: 'elastic-co', count: 10 }]); it('should render properly', async function () { - render(, { + render(, { initSeries, }); @@ -52,7 +52,7 @@ describe('Series Builder ReportDefinitionCol', function () { }); it('should render selected report definitions', async function () { - render(, { + render(, { initSeries, }); @@ -63,7 +63,7 @@ describe('Series Builder ReportDefinitionCol', function () { it('should be able to remove selected definition', async function () { const { setSeries } = render( - , + , { initSeries } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx index 47962af0d4bc4..0c620abf56e8a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -9,39 +9,40 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import styled from 'styled-components'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { CustomReportField } from '../custom_report_field'; -import { DataSeries, URLReportDefinition } from '../../types'; +import { ReportMetricOptions } from '../report_metric_options'; +import { SeriesConfig } from '../../types'; import { SeriesChartTypesSelect } from './chart_types'; import { OperationTypeSelect } from './operation_type_select'; import { DatePickerCol } from './date_picker_col'; import { parseCustomFieldName } from '../../configurations/lens_attributes'; import { ReportDefinitionField } from './report_definition_field'; -function getColumnType(dataView: DataSeries, selectedDefinition: URLReportDefinition) { - const { reportDefinitions } = dataView; - const customColumn = reportDefinitions.find((item) => item.custom); - if (customColumn?.field && selectedDefinition[customColumn?.field]) { - const { columnType } = parseCustomFieldName(customColumn.field, dataView, selectedDefinition); +function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { + const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); - return columnType; - } - return null; + return columnType; } export function ReportDefinitionCol({ - dataViewSeries, + seriesConfig, seriesId, }: { - dataViewSeries: DataSeries; + seriesConfig: SeriesConfig; seriesId: string; }) { const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); - const { reportDefinitions: selectedReportDefinitions = {} } = series ?? {}; + const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {}; - const { reportDefinitions, defaultSeriesType, hasOperationType, yAxisColumns } = dataViewSeries; + const { + definitionFields, + defaultSeriesType, + hasOperationType, + yAxisColumns, + metricOptions, + } = seriesConfig; const onChange = (field: string, value?: string[]) => { if (!value?.[0]) { @@ -58,7 +59,7 @@ export function ReportDefinitionCol({ } }; - const columnType = getColumnType(dataViewSeries, selectedReportDefinitions); + const columnType = getColumnType(seriesConfig, selectedMetricField); return ( @@ -66,20 +67,21 @@ export function ReportDefinitionCol({ - {reportDefinitions.map(({ field, custom, options }) => ( + {definitionFields.map((field) => ( - {!custom ? ( - - ) : ( - - )} + ))} + {metricOptions && ( + + + + )} {(hasOperationType || columnType === 'operation') && ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx index 51f4edaae93da..61f6f85dbeaf2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx @@ -15,16 +15,16 @@ import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearc import { PersistableFilter } from '../../../../../../../lens/common'; import { ExistsFilter } from '../../../../../../../../../src/plugins/data/common/es_query/filters'; import { buildPhrasesFilter } from '../../configurations/utils'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; interface Props { seriesId: string; field: string; - dataSeries: DataSeries; + seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; } -export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: Props) { +export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) { const { getSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -33,11 +33,11 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: const { reportDefinitions: selectedReportDefinitions = {} } = series; - const { labels, filters, reportDefinitions } = dataSeries; + const { labels, baseFilters, definitionFields } = seriesConfig; const queryFilters = useMemo(() => { const filtersN: ESFilter[] = []; - (filters ?? []).forEach((qFilter: PersistableFilter | ExistsFilter) => { + (baseFilters ?? []).forEach((qFilter: PersistableFilter | ExistsFilter) => { if (qFilter.query) { filtersN.push(qFilter.query); } @@ -48,8 +48,8 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: }); if (!isEmpty(selectedReportDefinitions)) { - reportDefinitions.forEach(({ field: fieldT, custom }) => { - if (!custom && indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { + definitionFields.forEach((fieldT) => { + if (indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { const values = selectedReportDefinitions?.[fieldT]; const valueFilter = buildPhrasesFilter(fieldT, values, indexPattern)[0]; filtersN.push(valueFilter.query); @@ -59,7 +59,7 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: return filtersN; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(selectedReportDefinitions), JSON.stringify(filters)]); + }, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]); return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx index f35639388aac5..0b183b5f20c03 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -21,7 +21,7 @@ describe('Series Builder ReportFilters', function () { }); it('should render properly', function () { - render(); + render(); screen.getByText('Add filter'); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx index 4571ecfe252e9..d5938c5387e8f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -7,23 +7,23 @@ import React from 'react'; import { SeriesFilter } from '../../series_editor/columns/series_filter'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; export function ReportFilters({ - dataViewSeries, + seriesConfig, seriesId, }: { - dataViewSeries: DataSeries; + seriesConfig: SeriesConfig; seriesId: string; }) { return ( ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx index f7cfe06c0d928..07048d47b2bc3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -38,7 +38,7 @@ describe('ReportTypesCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: 'user_agent.name', dataType: 'ux', - reportDefinitions: {}, + selectedMetricField: undefined, reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 64c7b48c668b8..396f8c4f1deb3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -15,7 +15,7 @@ import { ReportViewType, SeriesUrl } from '../../types'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { DEFAULT_TIME } from '../../configurations/constants'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { ReportTypeItem, SELECT_DATA_TYPE } from '../series_builder'; +import { ReportTypeItem } from '../series_builder'; interface Props { seriesId: string; @@ -30,7 +30,12 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) { const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); if (!restSeries.dataType) { - return {SELECT_DATA_TYPE}; + return ( + + ); } if (!loading && !hasData) { @@ -72,8 +77,7 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) { setSeries(seriesId, { ...restSeries, reportType, - operationType: undefined, - reportDefinitions: {}, + selectedMetricField: undefined, time: restSeries?.time ?? DEFAULT_TIME, }); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx similarity index 66% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx index 201df9628e135..a2a3e34c21834 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx @@ -8,28 +8,26 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { useSeriesStorage } from '../hooks/use_series_storage'; -import { ReportDefinition } from '../types'; +import { SeriesConfig } from '../types'; interface Props { - field: string; seriesId: string; defaultValue?: string; - options: ReportDefinition['options']; + options: SeriesConfig['metricOptions']; } -export function CustomReportField({ field, seriesId, options: opts }: Props) { +export function ReportMetricOptions({ seriesId, options: opts }: Props) { const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); - const { reportDefinitions: rtd = {} } = series; - const onChange = (value: string) => { - setSeries(seriesId, { ...series, reportDefinitions: { ...rtd, [field]: [value] } }); + setSeries(seriesId, { + ...series, + selectedMetricField: value, + }); }; - const { reportDefinitions } = series; - const options = opts ?? []; return ( @@ -41,7 +39,7 @@ export function CustomReportField({ field, seriesId, options: opts }: Props) { value: fd || id, inputDisplay: label, }))} - valueOfSelected={reportDefinitions?.[field]?.[0] || options?.[0].field || options?.[0].id} + valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id} onChange={(value) => onChange(value)} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx index e596eb6be354a..684cf3a210a51 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -17,7 +17,7 @@ import { EuiSwitch, } from '@elastic/eui'; import { rgba } from 'polished'; -import { AppDataType, DataSeries, ReportViewType, SeriesUrl } from '../types'; +import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; import { DataTypesCol } from './columns/data_types_col'; import { ReportTypesCol } from './columns/report_types_col'; import { ReportDefinitionCol } from './columns/report_definition_col'; @@ -66,7 +66,7 @@ export const ReportTypes: Record = { interface BuilderItem { id: string; series: SeriesUrl; - seriesConfig?: DataSeries; + seriesConfig?: SeriesConfig; } export function SeriesBuilder({ @@ -142,7 +142,7 @@ export function SeriesBuilder({ return loading ? ( LOADING_VIEW ) : reportType ? ( - + ) : ( SELECT_REPORT_TYPE ); @@ -159,7 +159,7 @@ export function SeriesBuilder({ field: 'id', render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => reportType && seriesConfig ? ( - + ) : null, }, { @@ -170,7 +170,7 @@ export function SeriesBuilder({ field: 'id', render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => reportType && seriesConfig ? ( - + ) : null, }, ...(multiSeries @@ -301,10 +301,3 @@ export const SELECT_REPORT_TYPE = i18n.translate( defaultMessage: 'No report type selected', } ); - -export const SELECT_DATA_TYPE = i18n.translate( - 'xpack.observability.expView.seriesBuilder.selectDataType', - { - defaultMessage: 'No data type selected', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx index a0d2fd86482a5..207a53e13f1ad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx @@ -8,22 +8,22 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Breakdowns } from './columns/breakdowns'; -import { DataSeries } from '../types'; +import { SeriesConfig } from '../types'; import { ChartOptions } from './columns/chart_options'; interface Props { - series: DataSeries; + seriesConfig: SeriesConfig; seriesId: string; - breakdowns: string[]; + breakdownFields: string[]; } -export function ChartEditOptions({ series, seriesId, breakdowns }: Props) { +export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) { return ( - + - + ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index d180bf4529c20..84568e1c5068a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -23,8 +23,8 @@ describe('Breakdowns', function () { render( ); @@ -37,8 +37,8 @@ describe('Breakdowns', function () { const { setSeries } = render( , { initSeries } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index cf24cb31951b1..2237935d466ad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -10,15 +10,15 @@ import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; interface Props { seriesId: string; breakdowns: string[]; - reportViewConfig: DataSeries; + seriesConfig: SeriesConfig; } -export function Breakdowns({ reportViewConfig, seriesId, breakdowns = [] }: Props) { +export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { const { setSeries, getSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -40,11 +40,11 @@ export function Breakdowns({ reportViewConfig, seriesId, breakdowns = [] }: Prop } }; - const hasUseBreakdownColumn = reportViewConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; + const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; const items = breakdowns.map((breakdown) => ({ id: breakdown, - label: reportViewConfig.labels[breakdown], + label: seriesConfig.labels[breakdown], })); if (!hasUseBreakdownColumn) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx index 08664ac75eb8d..f2a6377fd9b71 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx @@ -7,22 +7,25 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select'; import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types'; interface Props { - series: DataSeries; + seriesConfig: SeriesConfig; seriesId: string; } -export function ChartOptions({ series, seriesId }: Props) { +export function ChartOptions({ seriesConfig, seriesId }: Props) { return ( - + - {series.hasOperationType && ( + {seriesConfig.hasOperationType && ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 0f0cec0fbfcff..6f9d8efdc0681 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -14,7 +14,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { map } from 'lodash'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { DataSeries, UrlFilter } from '../../types'; +import { SeriesConfig, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; @@ -29,7 +29,7 @@ interface Props { isNegated?: boolean; goBack: () => void; nestedField?: string; - filters: DataSeries['filters']; + filters: SeriesConfig['baseFilters']; } export function FilterExpanded({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx index b7e20b341b572..02144c6929b38 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -16,16 +16,16 @@ import { EuiFlexGroup, } from '@elastic/eui'; import { FilterExpanded } from './filter_expanded'; -import { DataSeries } from '../../types'; +import { SeriesConfig } from '../../types'; import { FieldLabels } from '../../configurations/constants/constants'; import { SelectedFilters } from '../selected_filters'; import { useSeriesStorage } from '../../hooks/use_series_storage'; interface Props { seriesId: string; - defaultFilters: DataSeries['defaultFilters']; - filters: DataSeries['filters']; - series: DataSeries; + filterFields: SeriesConfig['filterFields']; + baseFilters: SeriesConfig['baseFilters']; + seriesConfig: SeriesConfig; isNew?: boolean; labels?: Record; } @@ -38,18 +38,18 @@ export interface Field { } export function SeriesFilter({ - series, + seriesConfig, isNew, seriesId, - defaultFilters = [], - filters, + filterFields = [], + baseFilters, labels, }: Props) { const [isPopoverVisible, setIsPopoverVisible] = useState(false); const [selectedField, setSelectedField] = useState(); - const options: Field[] = defaultFilters.map((field) => { + const options: Field[] = filterFields.map((field) => { if (typeof field === 'string') { return { label: labels?.[field] ?? FieldLabels[field], field }; } @@ -111,7 +111,7 @@ export function SeriesFilter({ goBack={() => { setSelectedField(undefined); }} - filters={filters} + filters={baseFilters} /> ) : null; @@ -122,7 +122,7 @@ export function SeriesFilter({ return ( - + , { initSeries }); + render(, { + initSeries, + }); await waitFor(() => { screen.getByText('Chrome'); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx index 33496e617a3a6..5d2ce6ba84951 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx @@ -9,28 +9,28 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSeriesStorage } from '../hooks/use_series_storage'; import { FilterLabel } from '../components/filter_label'; -import { DataSeries, UrlFilter } from '../types'; +import { SeriesConfig, UrlFilter } from '../types'; import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; interface Props { seriesId: string; - series: DataSeries; + seriesConfig: SeriesConfig; isNew?: boolean; } -export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props) { +export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) { const { getSeries } = useSeriesStorage(); const series = getSeries(seriesId); const { reportDefinitions = {} } = series; - const { labels } = dataSeries; + const { labels } = seriesConfig; const filters: UrlFilter[] = series.filters ?? []; - let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions, dataSeries); + let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); // we don't want to display report definition filters in new series view if (isNew) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index bcceeb204a31e..c3cc8484d1751 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { SeriesFilter } from './columns/series_filter'; -import { DataSeries } from '../types'; +import { SeriesConfig } from '../types'; import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { DatePickerCol } from './columns/date_picker_col'; @@ -19,7 +19,7 @@ import { SeriesActions } from './columns/series_actions'; import { ChartEditOptions } from './chart_edit_options'; interface EditItem { - seriesConfig: DataSeries; + seriesConfig: SeriesConfig; id: string; } @@ -48,10 +48,10 @@ export function SeriesEditor() { width: '15%', render: (seriesId: string, { seriesConfig, id }: EditItem) => ( ), }, @@ -64,8 +64,8 @@ export function SeriesEditor() { render: (seriesId: string, { seriesConfig, id }: EditItem) => ( ), }, @@ -123,7 +123,7 @@ export function SeriesEditor() { rowHeader="firstName" columns={columns} noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.seriesNotFound', { - defaultMessage: 'No series found, please add a series.', + defaultMessage: 'No series found. Please add a series.', })} cellProps={{ style: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index e8fccc5baab34..ad7c654c9a168 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -37,31 +37,27 @@ export interface ColumnFilter { query: string; } -export interface ReportDefinition { - field: string; - required?: boolean; - custom?: boolean; - options?: Array<{ - id: string; - field?: string; - label: string; - description?: string; - columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; - columnFilters?: ColumnFilter[]; - timeScale?: string; - }>; +export interface MetricOption { + id: string; + field?: string; + label: string; + description?: string; + columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; + columnFilters?: ColumnFilter[]; + timeScale?: string; } -export interface DataSeries { +export interface SeriesConfig { reportType: ReportViewType; xAxisColumn: Partial | Partial; yAxisColumns: Array>; - breakdowns: string[]; + breakdownFields: string[]; defaultSeriesType: SeriesType; - defaultFilters: Array; + filterFields: Array; seriesTypes: SeriesType[]; - filters?: PersistableFilter[] | ExistsFilter[]; - reportDefinitions: ReportDefinition[]; + baseFilters?: PersistableFilter[] | ExistsFilter[]; + definitionFields: string[]; + metricOptions?: MetricOption[]; labels: Record; hasOperationType: boolean; palette?: PaletteOutput; @@ -83,6 +79,7 @@ export interface SeriesUrl { operationType?: OperationType; dataType: AppDataType; reportDefinitions?: URLReportDefinition; + selectedMetricField?: string; isNew?: boolean; } diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 71bc241f65e10..dec2d409b020d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { SearchResponse } from 'elasticsearch'; import { isEmpty } from 'lodash'; @@ -19,7 +19,7 @@ import { useFormatUrl, } from '../../../common/components/link_to'; import { Ecs } from '../../../../common/ecs'; -import { Case } from '../../../../../cases/common'; +import { Case, CaseViewRefreshPropInterface } from '../../../../../cases/common'; import { TimelineId } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { KibanaServices, useKibana } from '../../../common/lib/kibana'; @@ -38,6 +38,7 @@ import { SEND_ALERT_TO_TIMELINE } from './translations'; import { useInsertTimeline } from '../use_insert_timeline'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline'; +import { CaseDetailsRefreshContext } from '../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context'; interface Props { caseId: string; @@ -176,9 +177,13 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = }) ); }, [dispatch]); + + const refreshRef = useRef(null); + return ( - <> + {casesUi.getCaseView({ + refreshRef, allCasesNavigation: { href: formattedAllCasesLink, onClick: async (e) => { @@ -247,7 +252,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = userCanCrud, })} - + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context.tsx new file mode 100644 index 0000000000000..8ae9d98a31429 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { MutableRefObject, useContext } from 'react'; +import { CaseViewRefreshPropInterface } from '../../../../../../cases/common'; + +/** + * React Context that can hold the `Ref` that is created an passed to `CaseViewProps['refreshRef`]`, enabling + * child components to trigger a refresh of a case. + */ +export const CaseDetailsRefreshContext = React.createContext | null>( + null +); + +/** + * Returns the closes CaseDetails Refresh interface if any. Used in conjuction with `CaseDetailsRefreshContext` component + * + * @example + * // Higher-order component + * const refreshRef = useRef(null); + * return .... + * + * // Now, use the hook from a hild component that was rendered inside of `` + * const caseDetailsRefresh = useWithCaseDetailsRefresh(); + * ... + * if (caseDetailsRefresh) { + * caseDetailsRefresh.refreshUserActionsAndComments(); + * } + */ +export const useWithCaseDetailsRefresh = (): Readonly | undefined => { + return useContext(CaseDetailsRefreshContext)?.current; +}; 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 ef311a7ca43b1..36443cc91f4e8 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 @@ -20,10 +20,12 @@ export const HostIsolationPanel = React.memo( ({ details, cancelCallback, + successCallback, isolateAction, }: { details: Maybe; cancelCallback: () => void; + successCallback?: () => void; isolateAction: string; }) => { const endpointId = useMemo(() => { @@ -92,6 +94,7 @@ export const HostIsolationPanel = React.memo( cases={associatedCases} casesInfo={casesInfo} cancelCallback={cancelCallback} + successCallback={successCallback} /> ) : ( ); } 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 b209c2f9c6e24..75dd850c30f43 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 @@ -24,12 +24,14 @@ export const IsolateHost = React.memo( cases, casesInfo, cancelCallback, + successCallback, }: { endpointId: string; hostName: string; cases: ReactNode; casesInfo: CasesFromAlertsResponse; cancelCallback: () => void; + successCallback?: () => void; }) => { const [comment, setComment] = useState(''); const [isIsolated, setIsIsolated] = useState(false); @@ -43,7 +45,11 @@ export const IsolateHost = React.memo( const confirmHostIsolation = useCallback(async () => { const hostIsolated = await isolateHost(); setIsIsolated(hostIsolated); - }, [isolateHost]); + + if (hostIsolated && successCallback) { + successCallback(); + } + }, [isolateHost, successCallback]); const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]); 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 ad8e8eaddb39e..2b810dc16eec8 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 @@ -24,12 +24,14 @@ export const UnisolateHost = React.memo( cases, casesInfo, cancelCallback, + successCallback, }: { endpointId: string; hostName: string; cases: ReactNode; casesInfo: CasesFromAlertsResponse; cancelCallback: () => void; + successCallback?: () => void; }) => { const [comment, setComment] = useState(''); const [isUnIsolated, setIsUnIsolated] = useState(false); @@ -41,9 +43,13 @@ export const UnisolateHost = React.memo( const { loading, unIsolateHost } = useHostUnisolation({ endpointId, comment, caseIds }); const confirmHostUnIsolation = useCallback(async () => { - const hostIsolated = await unIsolateHost(); - setIsUnIsolated(hostIsolated); - }, [unIsolateHost]); + const hostUnIsolated = await unIsolateHost(); + setIsUnIsolated(hostUnIsolated); + + if (hostUnIsolated && successCallback) { + successCallback(); + } + }, [successCallback, unIsolateHost]); const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx index 12426e05ba528..70d1d5ab5e194 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx @@ -12,6 +12,7 @@ import { createHostIsolation } from './api'; interface HostIsolationStatus { loading: boolean; + /** Boolean return will indicate if isolation action was created successful */ isolateHost: () => Promise; } diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts index 016170686c7dd..588eb9275ad21 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts @@ -66,6 +66,12 @@ export type EventFiltersFormStateChanged = Action<'eventFiltersFormStateChanged' payload: AsyncResourceState; }; +export type EventFiltersForceRefresh = Action<'eventFiltersForceRefresh'> & { + payload: { + forceRefresh: boolean; + }; +}; + export type EventFiltersPageAction = | EventFiltersListPageDataChanged | EventFiltersListPageDataExistsChanged @@ -81,4 +87,5 @@ export type EventFiltersPageAction = | EventFilterForDeletion | EventFilterDeletionReset | EventFilterDeleteSubmit - | EventFilterDeleteStatusChanged; + | EventFilterDeleteStatusChanged + | EventFiltersForceRefresh; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts index c1ade4e2cadec..ad9e3d32a3f4c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts @@ -232,9 +232,9 @@ const refreshListDataIfNeeded: MiddlewareActionHandler = async (store, eventFilt dispatch({ type: 'eventFiltersListPageDataChanged', payload: { - type: 'LoadingResourceState', // Ignore will be fixed with when AsyncResourceState is refactored (#830) // @ts-ignore + type: 'LoadingResourceState', previousState: getCurrentListPageDataState(state), }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts index 5366b6dcf155a..2bfc6b4378839 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts @@ -168,4 +168,35 @@ describe('event filters reducer', () => { }); }); }); + + describe('ForceRefresh', () => { + it('sets the force refresh state to true', () => { + const result = eventFiltersPageReducer( + { + ...initialState, + listPage: { ...initialState.listPage, forceRefresh: false }, + }, + { type: 'eventFiltersForceRefresh', payload: { forceRefresh: true } } + ); + + expect(result).toStrictEqual({ + ...initialState, + listPage: { ...initialState.listPage, forceRefresh: true }, + }); + }); + it('sets the force refresh state to false', () => { + const result = eventFiltersPageReducer( + { + ...initialState, + listPage: { ...initialState.listPage, forceRefresh: true }, + }, + { type: 'eventFiltersForceRefresh', payload: { forceRefresh: false } } + ); + + expect(result).toStrictEqual({ + ...initialState, + listPage: { ...initialState.listPage, forceRefresh: false }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts index 28292bdb1ed1c..b6e853ca4bf0f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts @@ -30,6 +30,7 @@ import { EventFilterForDeletion, EventFilterDeletionReset, EventFilterDeleteStatusChanged, + EventFiltersForceRefresh, } from './action'; import { initialEventFiltersPageState } from './builders'; @@ -220,6 +221,16 @@ const handleEventFilterDeleteStatusChanges: CaseReducer = (state, action) => { + return { + ...state, + listPage: { + ...state.listPage, + forceRefresh: action.payload.forceRefresh, + }, + }; +}; + export const eventFiltersPageReducer: StateReducer = ( state = initialEventFiltersPageState(), action @@ -237,6 +248,8 @@ export const eventFiltersPageReducer: StateReducer = ( return eventFiltersUpdateSuccess(state, action); case 'userChangedUrl': return userChangedUrl(state, action); + case 'eventFiltersForceRefresh': + return handleEventFilterForceRefresh(state, action); } // actions only handled if we're on the List Page diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts index fef6ccb99a17a..2fa196a053f78 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts @@ -184,8 +184,7 @@ export const listDataNeedsRefresh: EventFiltersSelector = createSelecto return ( forceRefresh || location.page_index + 1 !== currentQuery.page || - location.page_size !== currentQuery.perPage || - location.filter !== currentQuery.filter + location.page_size !== currentQuery.perPage ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts index 9d2d3c394c416..be3de3017d1f3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts @@ -250,11 +250,6 @@ describe('event filters selectors', () => { initialState.location.page_index = 10; expect(listDataNeedsRefresh(initialState)).toBe(true); }); - - it('should should return true if filter param differ from last api call', () => { - initialState.location.filter = 'query'; - expect(listDataNeedsRefresh(initialState)).toBe(true); - }); }); describe('getFormEntry()', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 0975104f02297..1f3b721fd51e3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -177,9 +177,13 @@ export const EventFiltersListPage = memo(() => { [navigateCallback] ); - const handleOnSearch = useCallback((query: string) => navigateCallback({ filter: query }), [ - navigateCallback, - ]); + const handleOnSearch = useCallback( + (query: string) => { + dispatch({ type: 'eventFiltersForceRefresh', payload: { forceRefresh: true } }); + navigateCallback({ filter: query }); + }, + [navigateCallback, dispatch] + ); return ( ; location: TrustedAppsListPageLocation; active: boolean; + forceRefresh: boolean; } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts index 34f48142c7032..a3f804ed6cd77 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -68,6 +68,12 @@ export type TrustedAppsPoliciesStateChanged = Action<'trustedAppsPoliciesStateCh payload: AsyncResourceState; }; +export type TrustedAppForceRefresh = Action<'trustedAppForceRefresh'> & { + payload: { + forceRefresh: boolean; + }; +}; + export type TrustedAppsPageAction = | TrustedAppsListDataOutdated | TrustedAppsListResourceStateChanged @@ -82,4 +88,5 @@ export type TrustedAppsPageAction = | TrustedAppCreationDialogConfirmed | TrustedAppsExistResponse | TrustedAppsPoliciesStateChanged - | TrustedAppCreationDialogClosed; + | TrustedAppCreationDialogClosed + | TrustedAppForceRefresh; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts index 5a22badec9afb..988d3d6e828cc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts @@ -59,4 +59,5 @@ export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({ filter: '', }, active: false, + forceRefresh: false, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index 878938aa20e1b..da6394a9ab896 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -80,6 +80,7 @@ const refreshListIfNeeded = async ( trustedAppsService: TrustedAppsService ) => { if (needsRefreshOfListData(store.getState())) { + store.dispatch({ type: 'trustedAppForceRefresh', payload: { forceRefresh: false } }); store.dispatch( createTrustedAppsListResourceStateChangedAction({ type: 'LoadingResourceState', @@ -395,11 +396,11 @@ const fetchEditTrustedAppIfNeeded = async ( dispatch({ type: 'trustedAppCreationEditItemStateChanged', payload: { - type: 'LoadingResourceState', // No easy way to get around this that I can see. `previousState` does not // seem to allow everything that `editItem` state can hold, so not even sure if using // type guards would work here // @ts-ignore + type: 'LoadingResourceState', previousState: editItemState(currentState)!, }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts index 58193eea3de52..42659e5cc3498 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts @@ -191,4 +191,29 @@ describe('reducer', () => { expect(result).toStrictEqual(initialState); }); }); + + describe('TrustedAppsForceRefresh', () => { + it('sets the force refresh state to true', () => { + const result = trustedAppsPageReducer( + { + ...initialState, + forceRefresh: false, + }, + { type: 'trustedAppForceRefresh', payload: { forceRefresh: true } } + ); + + expect(result).toStrictEqual({ ...initialState, forceRefresh: true }); + }); + it('sets the force refresh state to false', () => { + const result = trustedAppsPageReducer( + { + ...initialState, + forceRefresh: true, + }, + { type: 'trustedAppForceRefresh', payload: { forceRefresh: false } } + ); + + expect(result).toStrictEqual({ ...initialState, forceRefresh: false }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts index ea7bbb44c9bf2..d0de5dc80ee79 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts @@ -31,6 +31,7 @@ import { TrustedAppsExistResponse, TrustedAppsPoliciesStateChanged, TrustedAppCreationEditItemStateChanged, + TrustedAppForceRefresh, } from './action'; import { TrustedAppsListPageState } from '../state'; @@ -177,6 +178,13 @@ const updatePolicies: CaseReducer = (state, { p return state; }; +const forceRefresh: CaseReducer = (state, { payload }) => { + return { + ...state, + forceRefresh: payload.forceRefresh, + }; +}; + export const trustedAppsPageReducer: StateReducer = ( state = initialTrustedAppsPageState(), action @@ -226,6 +234,9 @@ export const trustedAppsPageReducer: StateReducer = ( case 'trustedAppsPoliciesStateChanged': return updatePolicies(state, action); + + case 'trustedAppForceRefresh': + return forceRefresh(state, action); } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index 43506f98193a0..338f30b447a8a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -30,16 +30,17 @@ export const needsRefreshOfListData = (state: Immutable { - return ( - data.pageIndex === location.page_index && - data.pageSize === location.page_size && - data.timestamp >= freshDataTimestamp && - data.filter === location.filter - ); - }) + (forceRefresh || + isOutdatedResourceState(currentPage, (data) => { + return ( + data.pageIndex === location.page_index && + data.pageSize === location.page_size && + data.timestamp >= freshDataTimestamp + ); + })) ); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 4cd6ad62f3a35..ec80b4c5ae21b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; +import React, { memo, useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; import { useLocation } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -33,6 +35,7 @@ import { TrustedAppsGrid } from './components/trusted_apps_grid'; import { TrustedAppsList } from './components/trusted_apps_list'; import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog'; import { TrustedAppsNotifications } from './trusted_apps_notifications'; +import { AppAction } from '../../../../common/store/actions'; import { ABOUT_TRUSTED_APPS, SEARCH_TRUSTED_APP_PLACEHOLDER } from './translations'; import { EmptyState } from './components/empty_state'; import { SearchBar } from '../../../components/search_bar'; @@ -40,11 +43,13 @@ import { BackToExternalAppButton } from '../../../components/back_to_external_ap import { ListPageRouteState } from '../../../../../common/endpoint/types'; export const TrustedAppsPage = memo(() => { + const dispatch = useDispatch>(); const { state: routeState } = useLocation(); const location = useTrustedAppsSelector(getCurrentLocation); const totalItemsCount = useTrustedAppsSelector(getListTotalItemsCount); const isCheckingIfEntriesExists = useTrustedAppsSelector(checkingIfEntriesExist); const doEntriesExist = useTrustedAppsSelector(entriesExist) === true; + const navigationCallback = useTrustedAppsNavigateCallback((query: string) => ({ filter: query })); const handleAddButtonClick = useTrustedAppsNavigateCallback(() => ({ show: 'create', id: undefined, @@ -56,7 +61,13 @@ export const TrustedAppsPage = memo(() => { const handleViewTypeChange = useTrustedAppsNavigateCallback((viewType: ViewType) => ({ view_type: viewType, })); - const handleOnSearch = useTrustedAppsNavigateCallback((query: string) => ({ filter: query })); + const handleOnSearch = useCallback( + (query: string) => { + dispatch({ type: 'trustedAppForceRefresh', payload: { forceRefresh: true } }); + navigationCallback(query); + }, + [dispatch, navigationCallback] + ); const showCreateFlyout = !!location.show; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 395538610f567..c4b19863ce7fc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -33,6 +33,7 @@ import { import { ALERT_DETAILS } from './translations'; import { useIsolationPrivileges } from '../../../../common/hooks/endpoint/use_isolate_privileges'; import { endpointAlertCheck } from '../../../../common/utils/endpoint_alert_check'; +import { useWithCaseDetailsRefresh } from '../../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` .euiFlyoutBody__overflow { @@ -121,6 +122,15 @@ const EventDetailsPanelComponent: React.FC = ({ ); }, [showAlertDetails, isolateAction]); + const caseDetailsRefresh = useWithCaseDetailsRefresh(); + + const handleIsolationActionSuccess = useCallback(() => { + // If a case details refresh ref is defined, then refresh actions and comments + if (caseDetailsRefresh) { + caseDetailsRefresh.refreshUserActionsAndComments(); + } + }, [caseDetailsRefresh]); + if (!expandedEvent?.eventId) { return null; } @@ -139,6 +149,7 @@ const EventDetailsPanelComponent: React.FC = ({ ) : ( 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 50fe2ffe2cea9..785434aa17ec6 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 @@ -61,6 +61,7 @@ export const isolationRequestHandler = function ( TypeOf, SecuritySolutionRequestHandlerContext > { + // eslint-disable-next-line complexity return async (context, req, res) => { if ( (!req.body.agent_ids || req.body.agent_ids.length === 0) && @@ -100,14 +101,14 @@ export const isolationRequestHandler = function ( } agentIDs = [...new Set(agentIDs)]; // dedupe + const casesClient = await endpointContext.service.getCasesClient(req); + // convert any alert IDs into cases 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) => { - const cases: CasesByAlertId = await ( - await endpointContext.service.getCasesClient(req) - ).cases.getCasesByAlertID({ + const cases: CasesByAlertId = await casesClient.cases.getCasesByAlertID({ alertID: a, options: { owner: APP_ID }, }); @@ -167,16 +168,21 @@ export const isolationRequestHandler = function ( commentLines.push(`\n\nWith Comment:\n> ${req.body.comment}`); } - caseIDs.forEach(async (caseId) => { - (await endpointContext.service.getCasesClient(req)).attachments.add({ - caseId, - comment: { - comment: commentLines.join('\n'), - type: CommentType.user, - owner: APP_ID, - }, - }); - }); + // Update all cases with a comment + if (caseIDs.length > 0) { + await Promise.all( + caseIDs.map((caseId) => + casesClient.attachments.add({ + caseId, + comment: { + comment: commentLines.join('\n'), + type: CommentType.user, + owner: APP_ID, + }, + }) + ) + ); + } return res.ok({ body: { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 22967ad94a083..1b44200566405 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7510,7 +7510,8 @@ "xpack.enterpriseSearch.appSearch.documentCreation.uploadJsonFile.title": ".jsonをドラッグアンドドロップ", "xpack.enterpriseSearch.appSearch.documentCreation.warningsTitle": "警告!", "xpack.enterpriseSearch.appSearch.documentDetail.confirmDelete": "このドキュメントを削除しますか?", - "xpack.enterpriseSearch.appSearch.documentDetail.deleteSuccess": "正常にドキュメントが削除に設定されました。すぐに削除されます。", + "xpack.enterpriseSearch.appSearch.documentDetail.deleteSuccess": "正常にドキュメントが削除に設定されました。", + "xpack.enterpriseSearch.appSearch.documentDetail.deleteSuccessDescription": "すぐに削除されます。", "xpack.enterpriseSearch.appSearch.documentDetail.fieldHeader": "フィールド", "xpack.enterpriseSearch.appSearch.documentDetail.title": "ドキュメント:{documentId}", "xpack.enterpriseSearch.appSearch.documentDetail.valueHeader": "値", @@ -7689,9 +7690,10 @@ "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.title": "フィールドを管理", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.weight.label": "重み", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteConfirmation": "このブーストを削除しますか?", - "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteSuccess": "関連性はデフォルト値にリセットされました。変更はすぐに結果に影響します。", + "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteSuccess": "関連性はデフォルト値にリセットされました。", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.resetConfirmation": "関連性のデフォルトを復元しますか?", - "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.updateSuccess": "関連性は正常に調整されました。変更はすぐに結果に影響します。", + "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.successDescription": "変更はすぐに結果に影響します。", + "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.updateSuccess": "関連性は正常に調整されました。", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.enterQueryMessage": "検索結果を表示するにはクエリを入力します", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.noResultsMessage": "一致するコンテンツが見つかりません", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.searchPlaceholder": "{engineName}を検索", @@ -7749,7 +7751,6 @@ "xpack.enterpriseSearch.appSearch.engineCreation.form.engineName.sanitizedNameHelpText": "エンジン名が変更されます", "xpack.enterpriseSearch.appSearch.engineCreation.form.submitButton.buttonLabel": "エンジンを作成", "xpack.enterpriseSearch.appSearch.engineCreation.form.title": "エンジン名を指定", - "xpack.enterpriseSearch.appSearch.engineCreation.successMessage": "エンジンが正常に作成されました。", "xpack.enterpriseSearch.appSearch.engineCreation.supportedLanguages.chineseDropDownOptionLabel": "中国語", "xpack.enterpriseSearch.appSearch.engineCreation.supportedLanguages.danishDropDownOptionLabel": "デンマーク語", "xpack.enterpriseSearch.appSearch.engineCreation.supportedLanguages.dutchDropDownOptionLabel": "オランダ語", @@ -7808,7 +7809,6 @@ "xpack.enterpriseSearch.appSearch.metaEngineCreation.form.sourceEngines.maxSourceEnginesWarningTitle": "メタエンジンのソースエンジンの上限は{maxEnginesPerMetaEngine}です", "xpack.enterpriseSearch.appSearch.metaEngineCreation.form.submitButton.buttonLabel": "メタエンジンを作成", "xpack.enterpriseSearch.appSearch.metaEngineCreation.form.title": "メタエンジン名を指定", - "xpack.enterpriseSearch.appSearch.metaEngineCreation.successMessage": "メタエンジンが正常に作成されました。", "xpack.enterpriseSearch.appSearch.metaEngineCreation.title": "メタエンジンを作成", "xpack.enterpriseSearch.appSearch.metaEngines.title": "メタエンジン", "xpack.enterpriseSearch.appSearch.multiInputRows.addValueButtonLabel": "値を追加", @@ -7824,10 +7824,6 @@ "xpack.enterpriseSearch.appSearch.roleMappingCreatedMessage": "ロールマッピングが正常に作成されました。", "xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage": "ロールマッピングが正常に削除されました", "xpack.enterpriseSearch.appSearch.roleMappingsEngineAccessHeading": "エンジンアクセス", - "xpack.enterpriseSearch.appSearch.roleMappingsResetButton": "マッピングをリセット", - "xpack.enterpriseSearch.appSearch.roleMappingsResetCancelButton": "キャンセル", - "xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmButton": "ロールマッピングをリセット", - "xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmTitle": "ロールマッピングをリセットしますか?", "xpack.enterpriseSearch.appSearch.roleMappingUpdatedMessage": "ロールマッピングが正常に更新されました。", "xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.buttonLabel": "サンプルエンジンを試す", "xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.description": "サンプルデータでエンジンをテストします。", @@ -7853,8 +7849,6 @@ "xpack.enterpriseSearch.appSearch.setupGuide.videoAlt": "App Searchの基本という短い動画では、App Searchを起動して実行する方法について説明します。", "xpack.enterpriseSearch.appSearch.tokens.admin.description": "資格情報APIとの連携では、非公開管理キーが使用されます。", "xpack.enterpriseSearch.appSearch.tokens.admin.name": "非公開管理キー", - "xpack.enterpriseSearch.appSearch.tokens.created": "キーの作成が正常に完了しました。", - "xpack.enterpriseSearch.appSearch.tokens.deleted": "正常にキーが削除されました。", "xpack.enterpriseSearch.appSearch.tokens.permissions.display.all": "すべて", "xpack.enterpriseSearch.appSearch.tokens.permissions.display.readonly": "読み取り専用", "xpack.enterpriseSearch.appSearch.tokens.permissions.display.readwrite": "読み取り/書き込み", @@ -7864,7 +7858,6 @@ "xpack.enterpriseSearch.appSearch.tokens.private.name": "非公開APIキー", "xpack.enterpriseSearch.appSearch.tokens.search.description": "エンドポイントのみの検索では、公開検索キーが使用されます。", "xpack.enterpriseSearch.appSearch.tokens.search.name": "公開検索キー", - "xpack.enterpriseSearch.appSearch.tokens.update": "正常に API キーを更新しました。", "xpack.enterpriseSearch.emailLabel": "メール", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.description": "場所を問わず、何でも検索。組織を支える多忙なチームのために、パワフルでモダンな検索エクスペリエンスを簡単に導入できます。Webサイトやアプリ、ワークプレイスに事前調整済みの検索をすばやく追加しましょう。何でもシンプルに検索できます。", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.notConfigured": "エンタープライズサーチはまだKibanaインスタンスで構成されていません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b1846be15ffa9..9636aa9ba5282 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7573,7 +7573,8 @@ "xpack.enterpriseSearch.appSearch.documentCreation.uploadJsonFile.title": "拖放 .json", "xpack.enterpriseSearch.appSearch.documentCreation.warningsTitle": "警告!", "xpack.enterpriseSearch.appSearch.documentDetail.confirmDelete": "是否确定要删除此文档?", - "xpack.enterpriseSearch.appSearch.documentDetail.deleteSuccess": "成功标记要删除的文档。文档将被暂时删除。", + "xpack.enterpriseSearch.appSearch.documentDetail.deleteSuccess": "成功标记要删除的文档。", + "xpack.enterpriseSearch.appSearch.documentDetail.deleteSuccessDescription": "文档将被暂时删除。", "xpack.enterpriseSearch.appSearch.documentDetail.fieldHeader": "字段", "xpack.enterpriseSearch.appSearch.documentDetail.title": "文档:{documentId}", "xpack.enterpriseSearch.appSearch.documentDetail.valueHeader": "值", @@ -7753,9 +7754,10 @@ "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.title": "管理字段", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.weight.label": "权重", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteConfirmation": "是否确定要删除此权重提升?", - "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteSuccess": "相关性已重置为默认值。更改将短暂地影响您的结果。", + "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteSuccess": "相关性已重置为默认值。", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.resetConfirmation": "确定要还原相关性默认值?", - "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.updateSuccess": "相关性已成功微调。更改将短暂地影响您的结果。", + "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.successDescription": "更改将短暂地影响您的结果。", + "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.updateSuccess": "相关性已成功微调。", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.enterQueryMessage": "输入查询以查看搜索结果", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.noResultsMessage": "未找到匹配内容", "xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.searchPlaceholder": "搜索 {engineName}", @@ -7814,7 +7816,6 @@ "xpack.enterpriseSearch.appSearch.engineCreation.form.engineName.sanitizedNameHelpText": "您的引擎将命名为", "xpack.enterpriseSearch.appSearch.engineCreation.form.submitButton.buttonLabel": "创建引擎", "xpack.enterpriseSearch.appSearch.engineCreation.form.title": "命名您的引擎", - "xpack.enterpriseSearch.appSearch.engineCreation.successMessage": "已成功创建引擎。", "xpack.enterpriseSearch.appSearch.engineCreation.supportedLanguages.chineseDropDownOptionLabel": "中文", "xpack.enterpriseSearch.appSearch.engineCreation.supportedLanguages.danishDropDownOptionLabel": "丹麦语", "xpack.enterpriseSearch.appSearch.engineCreation.supportedLanguages.dutchDropDownOptionLabel": "荷兰语", @@ -7875,7 +7876,6 @@ "xpack.enterpriseSearch.appSearch.metaEngineCreation.form.sourceEngines.maxSourceEnginesWarningTitle": "元引擎的源引擎数目限制为 {maxEnginesPerMetaEngine}", "xpack.enterpriseSearch.appSearch.metaEngineCreation.form.submitButton.buttonLabel": "创建元引擎", "xpack.enterpriseSearch.appSearch.metaEngineCreation.form.title": "命名您的元引擎", - "xpack.enterpriseSearch.appSearch.metaEngineCreation.successMessage": "已成功创建元引擎。", "xpack.enterpriseSearch.appSearch.metaEngineCreation.title": "创建元引擎", "xpack.enterpriseSearch.appSearch.metaEngines.title": "元引擎", "xpack.enterpriseSearch.appSearch.multiInputRows.addValueButtonLabel": "添加值", @@ -7892,10 +7892,6 @@ "xpack.enterpriseSearch.appSearch.roleMappingCreatedMessage": "角色映射已成功创建。", "xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage": "已成功删除角色映射", "xpack.enterpriseSearch.appSearch.roleMappingsEngineAccessHeading": "引擎访问", - "xpack.enterpriseSearch.appSearch.roleMappingsResetButton": "重置映射", - "xpack.enterpriseSearch.appSearch.roleMappingsResetCancelButton": "取消", - "xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmButton": "重置角色映射", - "xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmTitle": "确定要重置角色映射?", "xpack.enterpriseSearch.appSearch.roleMappingUpdatedMessage": "角色映射已成功更新。", "xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.buttonLabel": "试用示例引擎", "xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.description": "使用示例数据测试引擎。", @@ -7921,8 +7917,6 @@ "xpack.enterpriseSearch.appSearch.setupGuide.videoAlt": "App Search 入门 - 在此视频中,我们将指导您如何开始使用 App Search", "xpack.enterpriseSearch.appSearch.tokens.admin.description": "私有管理员密钥用于与凭据 API 进行交互。", "xpack.enterpriseSearch.appSearch.tokens.admin.name": "私有管理员密钥", - "xpack.enterpriseSearch.appSearch.tokens.created": "已成功创建密钥。", - "xpack.enterpriseSearch.appSearch.tokens.deleted": "已成功删除密钥。", "xpack.enterpriseSearch.appSearch.tokens.permissions.display.all": "全部", "xpack.enterpriseSearch.appSearch.tokens.permissions.display.readonly": "只读", "xpack.enterpriseSearch.appSearch.tokens.permissions.display.readwrite": "读取/写入", @@ -7932,7 +7926,6 @@ "xpack.enterpriseSearch.appSearch.tokens.private.name": "私有 API 密钥", "xpack.enterpriseSearch.appSearch.tokens.search.description": "公有搜索密钥仅用于搜索终端。", "xpack.enterpriseSearch.appSearch.tokens.search.name": "公有搜索密钥", - "xpack.enterpriseSearch.appSearch.tokens.update": "成功更新 API 密钥。", "xpack.enterpriseSearch.emailLabel": "电子邮件", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.description": "随时随地进行全面搜索。为工作繁忙的团队轻松实现强大的现代搜索体验。将预先调整的搜索功能快速添加到您的网站、应用或工作区。全面搜索就是这么简单。", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.notConfigured": "企业搜索尚未在您的 Kibana 实例中配置。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 01e63f2c60814..311166f09e466 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -69,6 +69,7 @@ const alertTypeFromApi = { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, minimumLicenseRequired: 'basic', + enabledInLicense: true, authorizedConsumers: { [ALERTS_FEATURE_ID]: { read: true, all: true }, }, @@ -520,3 +521,122 @@ describe('alerts_list with show only capability', () => { // TODO: check delete button }); }); + +describe('alerts_list with disabled itmes', () => { + let wrapper: ReactWrapper; + + async function setup() { + loadAlerts.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 2, + data: [ + { + id: '1', + name: 'test alert', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test alert type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + }, + { + id: '2', + name: 'test alert 2', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type_disabled_by_license', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'alert', params: { message: 'test' } }], + params: { name: 'test alert type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + }, + ], + }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + + loadAlertTypes.mockResolvedValue([ + alertTypeFromApi, + { + id: 'test_alert_type_disabled_by_license', + name: 'some alert type that is not allowed', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + producer: ALERTS_FEATURE_ID, + minimumLicenseRequired: 'platinum', + enabledInLicense: false, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + }, + }, + ]); + loadAllActions.mockResolvedValue([]); + + alertTypeRegistry.has.mockReturnValue(false); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.alertTypeRegistry = alertTypeRegistry; + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } + + it('renders rules list with disabled indicator if disabled due to license', async () => { + await setup(); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual(''); + expect(wrapper.find('EuiTableRow').at(1).prop('className')).toEqual( + 'actAlertsList__tableRowDisabled' + ); + expect(wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').length).toBe( + 1 + ); + expect( + wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().type + ).toEqual('questionInCircle'); + expect( + wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().content + ).toEqual('This rule type requires a Platinum license.'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 1fb688c4dd6bf..1c1633ff4a72f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -18,6 +18,7 @@ import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, + EuiIconTip, EuiSpacer, EuiLink, EuiEmptyPrompt, @@ -63,6 +64,7 @@ import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../common/constants'; import './alerts_list.scss'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; import { ManageLicenseModal } from './manage_license_modal'; +import { checkAlertTypeEnabled } from '../../../lib/check_alert_type_enabled'; const ENTER_KEY = 13; @@ -318,15 +320,32 @@ export const AlertsList: React.FunctionComponent = () => { width: '35%', 'data-test-subj': 'alertsTableCell-name', render: (name: string, alert: AlertTableItem) => { - return ( - { - history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); - }} - > - {name} - + const ruleType = alertTypesState.data.get(alert.alertTypeId); + const checkEnabledResult = checkAlertTypeEnabled(ruleType); + const link = ( + <> + { + history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); + }} + > + {name} + + + ); + return checkEnabledResult.isEnabled ? ( + link + ) : ( + <> + {link} + + ); }, }, diff --git a/yarn.lock b/yarn.lock index 9d7569b6ab4f2..2ea799810e3a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2768,6 +2768,10 @@ version "0.0.0" uid "" +"@kbn/spec-to-console@link:bazel-bin/packages/kbn-spec-to-console": + version "0.0.0" + uid "" + "@kbn/std@link:bazel-bin/packages/kbn-std": version "0.0.0" uid ""