diff --git a/.browserslistrc b/.browserslistrc index 36298c0f8cb93b..c54816e60aebe0 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -2,10 +2,10 @@ last 2 Firefox versions last 2 Chrome versions last 2 Safari versions -> 0.25% -not ie 11 -not op_mini all -not samsung 4 +last 2 Edge versions +last 1 ios_saf versions +last 1 and_chr versions +last 1 samsung versions [dev] last 1 chrome versions diff --git a/.ci/teamcity/default/build.sh b/.ci/teamcity/default/build.sh index af90e24ef5fe82..140233f29e6af9 100755 --- a/.ci/teamcity/default/build.sh +++ b/.ci/teamcity/default/build.sh @@ -14,6 +14,7 @@ node scripts/build_kibana_platform_plugins \ --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ --scan-dir "$XPACK_DIR/test/licensing_plugin/plugins" \ + --scan-dir "$XPACK_DIR/test/usage_collection/plugins" \ --verbose tc_end_block "Build Platform Plugins" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index efba93350b8fb7..2a5fc914662b63 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,7 +11,7 @@ Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) -- [ ] If a plugin configuration key changed, check if it needs to be whitelisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker) +- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) diff --git a/.teamcity/src/Common.kt b/.teamcity/src/Common.kt index 2e5357541bfe70..de3f96a5c790f5 100644 --- a/.teamcity/src/Common.kt +++ b/.teamcity/src/Common.kt @@ -22,6 +22,14 @@ fun isReportingEnabled(): Boolean { return ENABLE_REPORTING; } +// master and 7.x get committed to so often, we only want to run full CI for them hourly +// but for other branches, we can run daily and on merge +fun isHourlyOnlyBranch(): Boolean { + val branch = getProjectBranch() + + return branch == "master" || branch.matches("""^[0-9]+\.x$""".toRegex()) +} + fun makeSafeId(id: String): String { return id.replace(Regex("[^a-zA-Z0-9_]"), "_") } diff --git a/.teamcity/src/builds/DailyCi.kt b/.teamcity/src/builds/DailyCi.kt new file mode 100644 index 00000000000000..9a8f25f5ba0148 --- /dev/null +++ b/.teamcity/src/builds/DailyCi.kt @@ -0,0 +1,37 @@ +package builds + +import addSlackNotifications +import areTriggersEnabled +import dependsOn +import getProjectBranch +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.FailureAction +import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.schedule + +object DailyCi : BuildType({ + id("Daily_CI") + name = "Daily CI" + description = "Runs everything in CI, daily" + type = Type.COMPOSITE + paused = !areTriggersEnabled() + + triggers { + schedule { + schedulingPolicy = cron { + hours = "0" + minutes = "0" + } + branchFilter = "refs/heads/${getProjectBranch()}" + triggerBuild = always() + withPendingChangesOnly = false + } + } + + dependsOn( + FullCi + ) { + onDependencyCancel = FailureAction.ADD_PROBLEM + } + + addSlackNotifications() +}) diff --git a/.teamcity/src/builds/OnMergeCi.kt b/.teamcity/src/builds/OnMergeCi.kt new file mode 100644 index 00000000000000..174b73d53de615 --- /dev/null +++ b/.teamcity/src/builds/OnMergeCi.kt @@ -0,0 +1,34 @@ +package builds + +import addSlackNotifications +import areTriggersEnabled +import dependsOn +import getProjectBranch +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.FailureAction +import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs + +object OnMergeCi : BuildType({ + id("OnMerge_CI") + name = "On Merge CI" + description = "Runs everything in CI, on each commit" + type = Type.COMPOSITE + paused = !areTriggersEnabled() + + maxRunningBuilds = 1 + + triggers { + vcs { + perCheckinTriggering = false + branchFilter = "refs/heads/${getProjectBranch()}" + } + } + + dependsOn( + FullCi + ) { + onDependencyCancel = FailureAction.ADD_PROBLEM + } + + addSlackNotifications() +}) diff --git a/.teamcity/src/builds/default/DefaultFunctionalBase.kt b/.teamcity/src/builds/default/DefaultFunctionalBase.kt index d8124bd8521c0a..dc2f7756efeb58 100644 --- a/.teamcity/src/builds/default/DefaultFunctionalBase.kt +++ b/.teamcity/src/builds/default/DefaultFunctionalBase.kt @@ -1,6 +1,8 @@ package builds.default +import StandardAgents import addTestSettings +import co.elastic.teamcity.common.requireAgent import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType open class DefaultFunctionalBase(init: BuildType.() -> Unit = {}) : BuildType({ @@ -8,6 +10,8 @@ open class DefaultFunctionalBase(init: BuildType.() -> Unit = {}) : BuildType({ param("env.KBN_NP_PLUGINS_BUILT", "true") } + requireAgent(StandardAgents["4"]!!) + dependencies { defaultBuildWithPlugins() } diff --git a/.teamcity/src/projects/Kibana.kt b/.teamcity/src/projects/Kibana.kt index fe04d4f5ab36ec..5cddcf18e067ff 100644 --- a/.teamcity/src/projects/Kibana.kt +++ b/.teamcity/src/projects/Kibana.kt @@ -7,6 +7,7 @@ import builds.oss.* import builds.test.* import CloudProfile import co.elastic.teamcity.common.googleCloudProfile +import isHourlyOnlyBranch import jetbrains.buildServer.configs.kotlin.v2019_2.* import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.slackConnection import templates.KibanaTemplate @@ -136,7 +137,16 @@ fun Kibana(config: KibanaConfiguration = KibanaConfiguration()) : Project { buildType(FullCi) buildType(BaselineCi) - buildType(HourlyCi) + + // master and 7.x get committed to so often, we only want to run full CI for them hourly + // but for other branches, we can run daily and on merge + if (isHourlyOnlyBranch()) { + buildType(HourlyCi) + } else { + buildType(DailyCi) + buildType(OnMergeCi) + } + buildType(PullRequestCi) } diff --git a/dev_docs/assets/kibana_platform_plugin_end_user.png b/dev_docs/assets/kibana_platform_plugin_end_user.png new file mode 100644 index 00000000000000..a0e32a35ffe605 Binary files /dev/null and b/dev_docs/assets/kibana_platform_plugin_end_user.png differ diff --git a/dev_docs/assets/platform_plugin_cycle.png b/dev_docs/assets/platform_plugin_cycle.png new file mode 100644 index 00000000000000..533a38c4ed9c4d Binary files /dev/null and b/dev_docs/assets/platform_plugin_cycle.png differ diff --git a/dev_docs/kibana_platform_plugin_intro.mdx b/dev_docs/kibana_platform_plugin_intro.mdx new file mode 100644 index 00000000000000..3303561fae0696 --- /dev/null +++ b/dev_docs/kibana_platform_plugin_intro.mdx @@ -0,0 +1,305 @@ +--- +id: kibPlatformIntro +slug: /kibana-dev-docs/platform-intro +title: Plugins and the Kibana platform +summary: An introduction to the Kibana platform and how to use it to build a plugin. +date: 2021-01-06 +tags: ['kibana','onboarding', 'dev', 'architecture'] +--- + +From an end user perspective, Kibana is a tool for interacting with Elasticsearch, providing an easy way +to visualize and analyze data. + +From a developer perspective, Kibana is a platform that provides a set of tools to build not only the UI you see in Kibana today, but +a wide variety of applications that can be used to explore, visualize, and act upon data in Elasticsearch. The platform provides developers the ability to build applications, or inject extra functionality into +already existing applications. Did you know that almost everything you see in the +Kibana UI is built inside a plugin? If you removed all plugins from Kibana, you'd be left with an empty navigation menu, and a set of +developer tools. The Kibana platform is a blank canvas, just waiting for a developer to come along and create something! + +![Kibana personas](assets/kibana_platform_plugin_end_user.png) + +## Plugins vs The Platform + +The core platform provides the most basic and fundamental tools neccessary for building a plugin, like creating saved objects, +routing, application registration, and notifications. The Core platform is not a plugin itself, although +there are some plugins that provide platform functionality. For example, the + provides basic utilities to search, query, and filter data in Elasticsearch. +This code is not part of Core, but is still fundamental for building a plugin, + and we strongly encourage using this service over querying Elasticsearch directly. + + +We currently have three kinds of public services: + + - platform services provided by `core` + - platform services provided by plugins, that can, and should, be used by every plugin (e.g. ) . + - shared services provided by plugins, that are only relevant for only a few, specific plugins (e.g. "presentation utils"). + +Two common questions we encounter are: + +1. Which services are platform services? +2. What is the difference between platform code supplied by core, and platform code supplied by plugins? + +We don't have great answers to those questions today. Currently, the best answers we have are: + +1. Platform plugins are _usually_ plugins that are managed by the Platform Group, but we are starting to see some exceptions. +2. `core` code contains the most fundamental and stable services needed for plugin development. Everything else goes in a plugin. + +We will continue to focus on adding clarity around these types of services and what developers can expect from each. + + + + + +When the Kibana platform and plugin infrastructure was built, we thought of two types of code: core services, and other plugin services. We planned to keep the most stable and fundamental +code needed to build plugins inside core. + +In reality, we ended up with many platform-like services living outside of core, with no (short term) intention of moving them. We highly encourage plugin developers to use +them, so we consider them part of platform services. + +When we built our platform system, we also thought we'd end up with only a handful of large plugins outside core. Users could turn certain plugins off, to minimize the code + footprint and speed up Kibana. + +In reality, our plugin model ended up being used like micro-services. Plugins are the only form of encapsulation we provide developers, and they liked it! However, we ended + up with a ton of small plugins, that developers never intended to be uninstallable, nor tested in this manner. We are considering ways to provide developers the ability to build services + with the encapsulation + they desire, without the need to build a plugin. + +Another side effect of having many small plugins is that common code often ends up extracted into another plugin. Use case specific utilities are exported, + that are not meant to be used in a general manner. This makes our definition of "platform code" a bit trickier to define. We'd like to say "The platform is made up of + every publically exposed service", but in today's world, that wouldn't be a very accurate picture. + +We recognize the need to better clarify the relationship between core functionality, platform-like plugin functionality, and functionality exposed by other plugins. + It's something we will be working on! + + + +The main difference between core functionality and functionality supplied by plugins, is in how it is accessed. Core is +passed to plugins as the first parameter to their `start` and `setup` lifecycle functions, while plugin supplied functionality is passed as the +second parameter. Plugin dependencies must be declared explicitly inside the `kibana.json` file. Core functionality is always provided. Read the +section on for more information. + +## The anatomy of a plugin + +Plugins are defined as classes and present themselves to Kibana through a simple wrapper function. A plugin can have browser-side code, server-side code, +or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly, +and you interact with Core and other plugins in the same way. + +The basic file structure of a Kibana plugin named demo that has both client-side and server-side code would be: + +``` +plugins/ + demo + kibana.json [1] + public + index.ts [2] + plugin.ts [3] + server + index.ts [4] + plugin.ts [5] +``` + +### [1] kibana.json + +`kibana.json` is a static manifest file that is used to identify the plugin and to specify if this plugin has server-side code, browser-side code, or both: + +``` +{ + "id": "demo", + "version": "kibana", + "server": true, + "ui": true +} +``` + +### [2] public/index.ts + +`public/index.ts` is the entry point into the client-side code of this plugin. It must export a function named plugin, which will receive a standard set of + core capabilities as an argument. It should return an instance of its plugin class for Kibana to load. + +``` +import type { PluginInitializerContext } from 'kibana/server'; +import { DemoPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new DemoPlugin(initializerContext); +} +``` + +### [3] public/plugin.ts + +`public/plugin.ts` is the client-side plugin definition itself. Technically speaking, it does not need to be a class or even a separate file from the entry + point, but all plugins at Elastic should be consistent in this way. + + + ```ts +import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; + +export class DemoPlugin implements Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup) { + // called when plugin is setting up during Kibana's startup sequence + } + + public start(core: CoreStart) { + // called after all plugins are set up + } + + public stop() { + // called when plugin is torn down during Kibana's shutdown sequence + } +} + ``` + + +### [4] server/index.ts + +`server/index.ts` is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point: + +### [5] server/plugin.ts + +`server/plugin.ts` is the server-side plugin definition. The shape of this plugin is the same as it’s client-side counter-part: + +```ts +import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; + +export class DemoPlugin implements Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup) { + // called when plugin is setting up during Kibana's startup sequence + } + + public start(core: CoreStart) { + // called after all plugins are set up + } + + public stop() { + // called when plugin is torn down during Kibana's shutdown sequence + } +} +``` + +Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain +considerations related to how plugins integrate with core APIs and APIs exposed by other plugins that may greatly impact how they are built. + +## Plugin lifecycles & Core services + +The various independent domains that make up core are represented by a series of services. Those services expose public interfaces that are provided to all plugins. +Services expose different features at different parts of their lifecycle. We describe the lifecycle of core services and plugins with specifically-named functions on the service definition. + +Kibana has three lifecycles: setup, start, and stop. Each plugin’s setup function is called sequentially while Kibana is setting up on the server or when it is being loaded in the browser. The start functions are called sequentially after setup has been completed for all plugins. The stop functions are called sequentially while Kibana is gracefully shutting down the server or when the browser tab or window is being closed. + +The table below explains how each lifecycle relates to the state of Kibana. + +| lifecycle | purpose | server | browser | +| ---------- | ------ | ------- | ----- | +| setup | perform "registration" work to setup environment for runtime |configure REST API endpoint, register saved object types, etc. | configure application routes in SPA, register custom UI elements in extension points, etc. | +| start | bootstrap runtime logic | respond to an incoming request, request Elasticsearch server, etc. | start polling Kibana server, update DOM tree in response to user interactions, etc.| +| stop | cleanup runtime | dispose of active handles before the server shutdown. | store session data in the LocalStorage when the user navigates away from Kibana, etc. | + +Different service interfaces can and will be passed to setup, start, and stop because certain functionality makes sense in the context of a running plugin while other types +of functionality may have restrictions or may only make sense in the context of a plugin that is stopping. + +## How plugin's interact with each other, and Core + +The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. +For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler, +a plugin just accesses it off of the first argument: + +```ts +import type { CoreSetup } from 'kibana/server'; + +export class DemoPlugin { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + // handler is called when '/path' resource is requested with `GET` method + router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); + } +} +``` + +Unlike core, capabilities exposed by plugins are not automatically injected into all plugins. +Instead, if a plugin wishes to use the public interface provided by another plugin, it must first declare that plugin as a + dependency in it’s kibana.json manifest file. + +** foobar plugin.ts: ** + +``` +import type { Plugin } from 'kibana/server'; +export interface FoobarPluginSetup { [1] + getFoo(): string; +} + +export interface FoobarPluginStart { [1] + getBar(): string; +} + +export class MyPlugin implements Plugin { + public setup(): FoobarPluginSetup { + return { + getFoo() { + return 'foo'; + }, + }; + } + + public start(): FoobarPluginStart { + return { + getBar() { + return 'bar'; + }, + }; + } +} +``` +[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins. + + +** demo kibana.json** + +``` +{ + "id": "demo", + "requiredPlugins": ["foobar"], + "server": true, + "ui": true +} +``` + +With that specified in the plugin manifest, the appropriate interfaces are then available via the second argument of setup and/or start: + +```ts +import type { CoreSetup, CoreStart } from 'kibana/server'; +import type { FoobarPluginSetup, FoobarPluginStart } from '../../foobar/server'; + +interface DemoSetupPlugins { [1] + foobar: FoobarPluginSetup; +} + +interface DemoStartPlugins { + foobar: FoobarPluginStart; +} + +export class DemoPlugin { + public setup(core: CoreSetup, plugins: DemoSetupPlugins) { [2] + const { foobar } = plugins; + foobar.getFoo(); // 'foo' + foobar.getBar(); // throws because getBar does not exist + } + + public start(core: CoreStart, plugins: DemoStartPlugins) { [3] + const { foobar } = plugins; + foobar.getFoo(); // throws because getFoo does not exist + foobar.getBar(); // 'bar' + } + + public stop() {} +} +``` + +[1] The interface for plugin’s dependencies must be manually composed. You can do this by importing the appropriate type from the plugin and constructing an interface where the property name is the plugin’s ID. + +[2] These manually constructed types should then be used to specify the type of the second argument to the plugin. + +[3] Notice that the type for the setup and start lifecycles are different. Plugin lifecycle functions can only access the APIs that are exposed during that lifecycle. diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md new file mode 100644 index 00000000000000..5640395139ba69 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [addRuntimeField](./kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md) + +## IndexPattern.addRuntimeField() method + +Add a runtime field - Appended to existing mapped field or a new field is created as appropriate + +Signature: + +```typescript +addRuntimeField(name: string, runtimeField: RuntimeField): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | +| runtimeField | RuntimeField | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md index b318427012c0ac..48d94b84497bd9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md @@ -20,6 +20,7 @@ getAsSavedObjectBody(): { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }; ``` Returns: @@ -35,5 +36,6 @@ getAsSavedObjectBody(): { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md index 84aeb9ffeb21a0..37d31a35167dfc 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md @@ -14,6 +14,7 @@ getComputedFields(): { field: any; format: string; }[]; + runtimeFields: Record; }; ``` Returns: @@ -25,5 +26,6 @@ getComputedFields(): { field: any; format: string; }[]; + runtimeFields: Record; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 872e23e450f881..53d173d39f50d0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -45,6 +45,7 @@ export declare class IndexPattern implements IIndexPattern | Method | Modifiers | Description | | --- | --- | --- | +| [addRuntimeField(name, runtimeField)](./kibana-plugin-plugins-data-public.indexpattern.addruntimefield.md) | | Add a runtime field - Appended to existing mapped field or a new field is created as appropriate | | [addScriptedField(name, script, fieldType)](./kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md) | | Add scripted field to field list | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-public.indexpattern.getaggregationrestrictions.md) | | | | [getAsSavedObjectBody()](./kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md) | | Returns index pattern as saved object body for saving | @@ -58,6 +59,7 @@ export declare class IndexPattern implements IIndexPattern | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-public.indexpattern.istimenanosbased.md) | | | +| [removeRuntimeField(name)](./kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | | [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-public.indexpattern.setfieldattrs.md) | | | | [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-public.indexpattern.setfieldcount.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md new file mode 100644 index 00000000000000..7a5228fece782e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [removeRuntimeField](./kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md) + +## IndexPattern.removeRuntimeField() method + +Remove a runtime field - removed from mapped field or removed unmapped field as appropriate + +Signature: + +```typescript +removeRuntimeField(name: string): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md index 297bfa855f0ebc..41a4d3c55694b3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md @@ -21,6 +21,7 @@ export interface IndexPatternAttributes | [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-public.indexpatternattributes.fields.md) | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md) | string | | +| [runtimeFieldMap](./kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md) | string | | | [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternattributes.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.indexpatternattributes.title.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md new file mode 100644 index 00000000000000..0df7a9841e41f3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) > [runtimeFieldMap](./kibana-plugin-plugins-data-public.indexpatternattributes.runtimefieldmap.md) + +## IndexPatternAttributes.runtimeFieldMap property + +Signature: + +```typescript +runtimeFieldMap?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md new file mode 100644 index 00000000000000..653a1f2b39c290 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [isMapped](./kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md) + +## IndexPatternField.isMapped property + +Is the field part of the index mapping? + +Signature: + +```typescript +get isMapped(): boolean | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index c8118770ed3944..05c807b1cd8457 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -27,9 +27,11 @@ export declare class IndexPatternField implements IFieldType | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | undefined | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | +| [isMapped](./kibana-plugin-plugins-data-public.indexpatternfield.ismapped.md) | | boolean | undefined | Is the field part of the index mapping? | | [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | undefined | Script field language | | [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | | [readFromDocValues](./kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md) | | boolean | | +| [runtimeField](./kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md) | | RuntimeField | undefined | | | [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | undefined | Script field code | | [scripted](./kibana-plugin-plugins-data-public.indexpatternfield.scripted.md) | | boolean | | | [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md new file mode 100644 index 00000000000000..ad3b81eb23edce --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [runtimeField](./kibana-plugin-plugins-data-public.indexpatternfield.runtimefield.md) + +## IndexPatternField.runtimeField property + +Signature: + +```typescript +get runtimeField(): RuntimeField | undefined; + +set runtimeField(runtimeField: RuntimeField | undefined); +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md index c0fa165cfb115a..ae514e3fc6a8a3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md @@ -22,6 +22,7 @@ export interface IndexPatternSpec | [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) | IndexPatternFieldMap | | | [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) | string | saved object id | | [intervalName](./kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md) | string | | +| [runtimeFieldMap](./kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md) | Record<string, RuntimeField> | | | [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md) | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.indexpatternspec.title.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md new file mode 100644 index 00000000000000..e208760ff188f3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [runtimeFieldMap](./kibana-plugin-plugins-data-public.indexpatternspec.runtimefieldmap.md) + +## IndexPatternSpec.runtimeFieldMap property + +Signature: + +```typescript +runtimeFieldMap?: Record; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md new file mode 100644 index 00000000000000..ebd7f46d3598e9 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [addRuntimeField](./kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md) + +## IndexPattern.addRuntimeField() method + +Add a runtime field - Appended to existing mapped field or a new field is created as appropriate + +Signature: + +```typescript +addRuntimeField(name: string, runtimeField: RuntimeField): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | +| runtimeField | RuntimeField | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md index 7d70af4b535fea..668d563ff04c0e 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md @@ -20,6 +20,7 @@ getAsSavedObjectBody(): { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }; ``` Returns: @@ -35,5 +36,6 @@ getAsSavedObjectBody(): { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md index eab6ae9bf90331..0030adf1261e47 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md @@ -14,6 +14,7 @@ getComputedFields(): { field: any; format: string; }[]; + runtimeFields: Record; }; ``` Returns: @@ -25,5 +26,6 @@ getComputedFields(): { field: any; format: string; }[]; + runtimeFields: Record; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 70c37ba1b39262..97d1cd91152623 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -45,6 +45,7 @@ export declare class IndexPattern implements IIndexPattern | Method | Modifiers | Description | | --- | --- | --- | +| [addRuntimeField(name, runtimeField)](./kibana-plugin-plugins-data-server.indexpattern.addruntimefield.md) | | Add a runtime field - Appended to existing mapped field or a new field is created as appropriate | | [addScriptedField(name, script, fieldType)](./kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md) | | Add scripted field to field list | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md) | | | | [getAsSavedObjectBody()](./kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md) | | Returns index pattern as saved object body for saving | @@ -58,6 +59,7 @@ export declare class IndexPattern implements IIndexPattern | [getTimeField()](./kibana-plugin-plugins-data-server.indexpattern.gettimefield.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) | | | +| [removeRuntimeField(name)](./kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | | [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-server.indexpattern.setfieldattrs.md) | | | | [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-server.indexpattern.setfieldcount.md) | | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md new file mode 100644 index 00000000000000..da8e7e40a7fac2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [removeRuntimeField](./kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md) + +## IndexPattern.removeRuntimeField() method + +Remove a runtime field - removed from mapped field or removed unmapped field as appropriate + +Signature: + +```typescript +removeRuntimeField(name: string): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md index bfc7f65425f9c4..20af97ecc8761f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md @@ -21,6 +21,7 @@ export interface IndexPatternAttributes | [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-server.indexpatternattributes.fields.md) | string | | | [intervalName](./kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md) | string | | +| [runtimeFieldMap](./kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md) | string | | | [sourceFilters](./kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-server.indexpatternattributes.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-server.indexpatternattributes.title.md) | string | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md new file mode 100644 index 00000000000000..1e0dff2ad0e46b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) > [runtimeFieldMap](./kibana-plugin-plugins-data-server.indexpatternattributes.runtimefieldmap.md) + +## IndexPatternAttributes.runtimeFieldMap property + +Signature: + +```typescript +runtimeFieldMap?: string; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md new file mode 100644 index 00000000000000..0fdf36bc719ec3 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) > [ast](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md) + +## ExpressionsInspectorAdapter.ast property + +Signature: + +```typescript +get ast(): any; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md new file mode 100644 index 00000000000000..671270a5c78ce9 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) > [logAST](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md) + +## ExpressionsInspectorAdapter.logAST() method + +Signature: + +```typescript +logAST(ast: any): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | any | | + +Returns: + +`void` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md new file mode 100644 index 00000000000000..23d542a0f69eb5 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) + +## ExpressionsInspectorAdapter class + +Signature: + +```typescript +export declare class ExpressionsInspectorAdapter extends EventEmitter +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [ast](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md) | | any | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [logAST(ast)](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md) | | | + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md index 1b97c9e11f83ca..e3eb7a34175eee 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md @@ -16,6 +16,7 @@ | [ExpressionRenderer](./kibana-plugin-plugins-expressions-public.expressionrenderer.md) | | | [ExpressionRendererRegistry](./kibana-plugin-plugins-expressions-public.expressionrendererregistry.md) | | | [ExpressionRenderHandler](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.md) | | +| [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) | | | [ExpressionsPublicPlugin](./kibana-plugin-plugins-expressions-public.expressionspublicplugin.md) | | | [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) | ExpressionsService class is used for multiple purposes:1. It implements the same Expressions service that can be used on both: (1) server-side and (2) browser-side. 2. It implements the same Expressions service that users can fork/clone, thus have their own instance of the Expressions plugin. 3. ExpressionsService defines the public contracts of \*setup\* and \*start\* Kibana Platform life-cycles for ease-of-use on server-side and browser-side. 4. ExpressionsService creates a bound version of all exported contract functions. 5. Functions are bound the way there are:\`\`\`ts registerFunction = (...args: Parameters<Executor\['registerFunction'\]> ): ReturnType<Executor\['registerFunction'\]> => this.executor.registerFunction(...args); \`\`\`so that JSDoc appears in developers IDE when they use those plugins.expressions.registerFunction(. | | [ExpressionType](./kibana-plugin-plugins-expressions-public.expressiontype.md) | | diff --git a/docs/index.asciidoc b/docs/index.asciidoc index b91af2ab51ebf8..eb6f794434f8a1 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -7,7 +7,7 @@ :blog-ref: https://www.elastic.co/blog/ :wikipedia: https://en.wikipedia.org/wiki -include::{docs-root}/shared/versions/stack/7.10.asciidoc[] +include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] :docker-repo: docker.elastic.co/kibana/kibana :docker-image: docker.elastic.co/kibana/kibana:{version} diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 7e7c8953fd527d..bf4f7d9d827047 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -134,11 +134,6 @@ The maximum numbers of buckets that a single data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). -[[pagenavigation]]`pageNavigation`:: -The style of navigation menu for Kibana. Choices are Legacy, the legacy style -where every plugin is represented in the nav, and Modern, a new format that -bundles related plugins together in flyaway nested navigation. - [[query-allowleadingwildcards]]`query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character in a query clause. Only applies when experimental query features are enabled in the query bar. To disallow @@ -458,7 +453,7 @@ of buckets to try to represent. [horizontal] [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: -Enables legacy charts library for area, line and bar charts in visualize. Currently, only legacy charts library supports split chart aggregation. +Enables legacy charts library for area, line and bar charts in visualize. [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index d83f2571ad26f8..28dbacc628ce98 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -158,10 +158,13 @@ date values in {es}, you can use a {kib} field formatter to change the display t <>, and <>. +To customize the displayed field name provided by {es}, you can +use *Custom Label* . + A popularity counter keeps track of the fields you use most often. The top five most popular fields and their values are displayed in <>. -To edit the field format and popularity counter, click the edit icon +To edit the field display, click the edit icon (image:management/index-patterns/images/edit_icon.png[]) in the index pattern detail view. [role="screenshot"] diff --git a/docs/management/index-patterns/images/edit-field-format.png b/docs/management/index-patterns/images/edit-field-format.png old mode 100755 new mode 100644 index 15ab0c5bf87639..1ad29d82d2590d Binary files a/docs/management/index-patterns/images/edit-field-format.png and b/docs/management/index-patterns/images/edit-field-format.png differ diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index 1b9d22699d359e..97cd184a4db884 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -18,6 +18,7 @@ To format a field: . Open the main menu, and click *Stack Management > Index Patterns*. . Click the index pattern that contains the field you want to format. . Find the field you want to format and click the edit icon (image:management/index-patterns/images/edit_icon.png[]). +. Enter a custom label for the field, if needed. . Select a format and fill in the details. + [role="screenshot"] diff --git a/docs/management/managing-tags.asciidoc b/docs/management/managing-tags.asciidoc index 3da98b2281fdc4..88fdef66a74183 100644 --- a/docs/management/managing-tags.asciidoc +++ b/docs/management/managing-tags.asciidoc @@ -37,8 +37,7 @@ Create a tag to assign to your saved objects. image::images/tags/create-tag.png[Tag creation popin] . Enter a name and select a color for the new tag. + -The name can include alphanumeric characters (English letters and digits), `:`, `-`, `_` and the space character, -and cannot be longer than 50 characters. +The name cannot be longer than 50 characters. . Click *Create tag*. [float] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 6dd76f782d6686..26f095c59c6444 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -205,6 +205,9 @@ the username and password that the {kib} server uses to perform maintenance on the {kib} index at startup. {kib} users still need to authenticate with {es}, which is proxied through the {kib} server. +| `enterpriseSearch.host` + | The URL of your Enterprise Search instance + | `interpreter.enableInVisualize` | Enables use of interpreter in Visualize. *Default: `true`* diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index 74d097164c4a78..7436536d227817 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -1,11 +1,13 @@ [[upgrade-migrations]] === Upgrade migrations -Every time {kib} is upgraded it checks to see if all saved objects, such as dashboards, visualizations, and index patterns, are compatible with the new version. If any saved objects need to be updated, then the automatic saved object migration process is kicked off. +Every time {kib} is upgraded it will perform an upgrade migration to ensure that all <> are compatible with the new version. NOTE: 6.7 includes an https://www.elastic.co/guide/en/kibana/6.7/upgrade-assistant.html[Upgrade Assistant] to help you prepare for your upgrade to 7.0. To access the assistant, go to *Management > 7.0 Upgrade Assistant*. +WARNING: {kib} 7.12.0 and later uses a new migration process and index naming scheme. Be sure to read the documentation for your version of {kib} before proceeding. + WARNING: The following instructions assumes {kib} is using the default index names. If the `kibana.index` or `xpack.tasks.index` configuration settings were changed these instructions will have to be adapted accordingly. [float] @@ -14,19 +16,35 @@ WARNING: The following instructions assumes {kib} is using the default index nam Saved objects are stored in two indices: -* `.kibana_N`, or if set, the `kibana.index` configuration setting -* `.kibana_task_manager_N`, or if set, the `xpack.tasks.index` configuration setting +* `.kibana_{kibana_version}_001`, or if the `kibana.index` configuration setting is set `.{kibana.index}_{kibana_version}_001`. E.g. for Kibana v7.12.0 `.kibana_7.12.0_001`. +* `.kibana_task_manager_{kibana_version}_001`, or if the `xpack.tasks.index` configuration setting is set `.{xpack.tasks.index}_{kibana_version}_001` E.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`. -For each of these indices, `N` is a number that increments every time {kib} runs an upgrade migration on that index. The index aliases `.kibana` and `.kibana_task_manager` point to the most up-to-date index. +The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date version indices. + +The first time a newer {kib} starts, it will first perform an upgrade migration before starting plugins or serving HTTP traffic. To prevent losing acknowledged writes old nodes should be shutdown before starting the upgrade. To reduce the likelihood of old nodes losing acknowledged writes, {kib} 7.12.0 and later will add a write block to the outdated index. Table 1 lists the saved objects indices used by previous versions of {kib}. + +.Saved object indices and aliases per {kib} version +[options="header"] +[cols="a,a,a"] +|======================= +|Upgrading from version | Outdated index (alias) | Upgraded index (alias) +| 6.0.0 through 6.4.x | `.kibana` 1.3+^.^| `.kibana_7.12.0_001` +(`.kibana` alias) + +`.kibana_task_manager_7.12.0_001` (`.kibana_task_manager` alias) +| 6.5.0 through 7.3.x | `.kibana_N` (`.kibana` alias) +| 7.4.0 through 7.11.x +| `.kibana_N` (`.kibana` alias) -While {kib} is starting up and before serving any HTTP traffic, it checks to see if any internal mapping changes or data transformations for existing saved objects are required. +`.kibana_task_manager_N` (`.kibana_task_manager` alias) +|======================= -When changes are necessary, a new migration is started. To ensure that only one {kib} instance performs the migration, each instance will attempt to obtain a migration lock by creating a new `.kibana_N+1` index. The instance that succeeds in creating the index will then read batches of documents from the existing index, migrate them, and write them to the new index. Once the objects are migrated, the lock is released by pointing the `.kibana` index alias the new upgraded `.kibana_N+1` index. +==== Upgrading multiple {kib} instances +When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. -Instances that failed to acquire a lock will log `Another Kibana instance appears to be migrating the index. Waiting for that migration to complete`. The instance will then wait until `.kibana` points to an upgraded index before starting up and serving HTTP traffic. +Kibana does not support rolling upgrades. However, once outdated instances are shutdown, all upgraded instances can be started in parallel in which case all instances will participate in the upgrade migration in parallel. -NOTE: Prior to 6.5.0, saved objects were stored directly in an index named `.kibana`. After upgrading to version 6.5+, {kib} will migrate this index into `.kibana_N` and set `.kibana` up as an index alias. + -Prior to 7.4.0, task manager tasks were stored directly in an index name `.kibana_task_manager`. After upgrading to version 7.4+, {kib} will migrate this index into `.kibana_task_manager_N` and set `.kibana_task_manager` up as an index alias. +For large deployments with more than 10 {kib} instances and more than 10 000 saved objects, the upgrade downtime can be reduced by bringing up a single {kib} instance and waiting for it to complete the upgrade migration before bringing up the remaining instances. [float] [[preventing-migration-failures]] @@ -54,50 +72,31 @@ Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensu * a "green" cluster status [float] -===== Running different versions of {kib} connected to the same {es} index -Kibana does not support rolling upgrades. Stop all {kib} instances before starting a newer version to prevent upgrade failures and data loss. +===== Different versions of {kib} connected to the same {es} index +When different versions of {kib} are attempting an upgrade migration in parallel this can lead to migration failures. Ensure that all {kib} instances are running the same version, configuration and plugins. [float] ===== Incompatible `xpack.tasks.index` configuration setting -For {kib} < 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting. +For {kib} versions prior to 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting. [float] [[resolve-migrations-failures]] ==== Resolving migration failures -If {kib} terminates unexpectedly while migrating a saved object index, manual intervention is required before {kib} will attempt to perform the migration again. Follow the advice in (preventing migration failures)[preventing-migration-failures] before retrying a migration upgrade. - -As mentioned above, {kib} will create a migration lock for each index that requires a migration by creating a new `.kibana_N+1` index. For example: if the `.kibana_task_manager` alias is pointing to `.kibana_task_manager_5` then the first {kib} that succeeds in creating `.kibana_task_manager_6` will obtain the lock to start migrations. - -However, if the instance that obtained the lock fails to migrate the index, all other {kib} instances will be blocked from performing this migration. This includes the instance that originally obtained the lock, it will be blocked from retrying the migration even when restarted. - -[float] -===== Retry a migration by restoring a backup snapshot: - -1. Before proceeding ensure that you have a recent and successful backup snapshot of all `.kibana*` indices. -2. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. -3. Delete all saved object indices with `DELETE /.kibana*` -4. Restore the `.kibana* indices and their aliases from the backup snapshot. See {es} {ref}/modules-snapshots.html[snapshots] -5. Start up all {kib} instances to retry the upgrade migration. - -[float] -===== (Not recommended) Retry a migration without a backup snapshot: +If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to perform the migration again once the process has restarted. Do not delete any saved objects indices to attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and later does not require deleting any indices to release a failed migration lock. -1. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. -2. Identify any migration locks by comparing the output of `GET /_cat/aliases` and `GET /_cat/indices`. If e.g. `.kibana` is pointing to `.kibana_4` and there is a `.kibana_5` index, the `.kibana_5` index will act like a migration lock blocking further attempts. Be sure to check both the `.kibana` and `.kibana_task_manager` aliases and their indices. -3. Remove any migration locks e.g. `DELETE /.kibana_5`. -4. Start up all {kib} instances. +If upgrade migrations fail repeatedly, follow the advice in (preventing migration failures)[preventing-migration-failures]. Once the root cause for the migration failure has been addressed, {kib} will automatically retry the migration without any further intervention. If you're unable to resolve a failed migration following these steps, please contact support. [float] [[upgrade-migrations-rolling-back]] ==== Rolling back to a previous version of {kib} -If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify the root cause. +If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify and fix the root cause. WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. + Any changes made after an upgrade will be lost when rolling back to a previous version. -In order to rollback after a failed upgrade migration, the saved object indices might also have to be rolled back to be compatible with the previous {kibana} version. +In order to rollback after a failed upgrade migration, the saved object indices have to be rolled back to be compatible with the previous {kibana} version. [float] ===== Rollback by restoring a backup snapshot: @@ -111,17 +110,15 @@ In order to rollback after a failed upgrade migration, the saved object indices [float] ===== (Not recommended) Rollback without a backup snapshot: -WARNING: {kib} does not run a migration for every saved object index on every upgrade. A {kib} version upgrade can cause no migrations, migrate only the `.kibana` or the `.kibana_task_manager` index or both. Carefully read the logs to ensure that you're only deleting indices created by a later version of {kib} to avoid data loss. - 1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. 2. Create a backup snapshot of the `.kibana*` indices. -3. Use the logs from the upgraded instances to identify which indices {kib} attempted to upgrade. The server logs will contain an entry like `[savedobjects-service] Creating index .kibana_4.` and/or `[savedobjects-service] Creating index .kibana_task_manager_2.` If no indices were created after upgrading {kib} then no further action is required to perform a rollback, skip ahead to step (5). If you're running multiple {kib} instances, be sure to inspect all instances' logs. -4. Delete each of the indices identified in step (2). e.g. `DELETE /.kibana_task_manager_2` -5. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +3. Delete the version specific indices created by the failed upgrade migration. E.g. if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` +4. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +5. Remove the write block from the rollback indices. `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` 6. Start up {kib} on the older version you wish to rollback to. [float] [[upgrade-migrations-old-indices]] ==== Handling old `.kibana_N` indices -After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, etc). {kib} only uses the index that the `.kibana` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. \ No newline at end of file +After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, `.kibana_7.12.0` etc). {kib} only uses the index that the `.kibana` and `.kibana_task_manager` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. \ No newline at end of file diff --git a/docs/setup/upgrade/upgrade-standard.asciidoc b/docs/setup/upgrade/upgrade-standard.asciidoc index b27bb8867e624c..b43da6aef97657 100644 --- a/docs/setup/upgrade/upgrade-standard.asciidoc +++ b/docs/setup/upgrade/upgrade-standard.asciidoc @@ -15,17 +15,17 @@ necessary remediation steps as per those instructions. [float] ==== Upgrading multiple {kib} instances -WARNING: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading. +NOTE: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading. -Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause upgrade migration failures and data loss. This is because acknowledged writes from the older instances could be written into the _old_ index while the migration is in progress. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version. - -The first instance that triggers saved object migrations will run the entire process. Any other instances started up while a migration is running will log a message and then wait until saved object migrations has completed before they start serving HTTP traffic. +Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause data loss. This is because older instances will continue to write saved objects in a different format than the newer instances. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version. [float] ==== Upgrade using a `deb` or `rpm` package . Stop the existing {kib} process using the appropriate command for your - system. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss. + system. If you have multiple {kib} instances connecting to the same {es} + cluster ensure that all instances are stopped before proceeding to the next + step to avoid data loss. . Use `rpm` or `dpkg` to install the new package. All files should be placed in their proper locations and config files should not be overwritten. + @@ -65,5 +65,7 @@ and becomes a new instance in the monitoring data. . Install the appropriate versions of all your plugins for your new installation using the `kibana-plugin` script. Check out the <> documentation for more information. -. Stop the old {kib} process. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss. +. Stop the old {kib} process. If you have multiple {kib} instances connecting + to the same {es} cluster ensure that all instances are stopped before + proceeding to the next step to avoid data loss. . Start the new {kib} process. diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 7de5ff56228cc0..7c5a957d1cf794 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[alert-types]] -== Alert types +== Standard stack alert types {kib} supplies alert types in two ways: some are built into {kib} (these are known as stack alerts), while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. diff --git a/docs/user/alerting/geo-alert-types.asciidoc b/docs/user/alerting/geo-alert-types.asciidoc new file mode 100644 index 00000000000000..c04cf4bca4320d --- /dev/null +++ b/docs/user/alerting/geo-alert-types.asciidoc @@ -0,0 +1,127 @@ +[role="xpack"] +[[geo-alert-types]] +== Geo alert types + +experimental[] Two additional stack alerts are available: +<> and <>. To enable, +add the following configuration to your `kibana.yml`: + +```yml +xpack.stack_alerts.enableGeoAlerting: true +``` + +As with other stack alerts, you need `all` access to the *Stack Alerts* feature +to be able to create and edit either of the geo alerts. +See <> for more information on configuring roles that provide access to this feature. + +[float] +=== Geo alert requirements + +To create either a *Tracking threshold* or a *Tracking containment* alert, the +following requirements must be present: + +- *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, +and some form of entity identifier. An entity identifier is a `keyword` or `number` +field that consistently identifies the entity to be tracked. The data in this index should be dynamically +updating so that there are entity movements to alert upon. +- *Boundaries index or index pattern*: An index containing `geo_shape` data, such as boundary data and bounding box data. +This data is presumed to be static (not updating). Shape data matching the query is +harvested once when the alert is created and anytime after when the alert is re-enabled +after disablement. + +By design, current interval entity locations (_current_ is determined by `date` in +the *Tracked index or index pattern*) are queried to determine if they are contained +within any monitored boundaries. Entity +data should be somewhat "real time", meaning the dates of new documents aren’t older +than the current time minus the amount of the interval. If data older than +`now - ` is ingested, it won't trigger an alert. + +[float] +=== Creating a geo alert +Both *threshold* and *containment* alerts can be created by clicking the *Create* +button in the <>. +Complete the <>. +Select <> to generate an alert when an entity crosses a boundary, and you desire the +ability to highlight lines of crossing on a custom map. +Select +<> if an entity should send out constant alerts +while contained within a boundary (this feature is optional) or if the alert is generally +just more focused around activity when an entity exists within a shape. + +[role="screenshot"] +image::images/alert-types-tracking-select.png[Choosing a tracking alert type] + +[NOTE] +================================================== +With recent advances in the alerting framework, most of the features +available in Tracking threshold alerts can be replicated with just +a little more work in Tracking containment alerts. The capabilities of Tracking +threshold alerts may be deprecated or folded into Tracking containment alerts +in the future. +================================================== + +[float] +[[alert-type-tracking-threshold]] +=== Tracking threshold +The Tracking threshold alert type runs an {es} query over indices, comparing the latest +entity locations with their previous locations. In the event that an entity has crossed a +boundary from the selected boundary index, an alert may be generated. + +[float] +==== Defining the conditions +Tracking threshold has a *Delayed evaluation offset* and 4 clauses that define the +condition to detect, as well as 2 Kuery bars used to provide additional filtering +context for each of the indices. + +[role="screenshot"] +image::images/alert-types-tracking-threshold-conditions.png[Five clauses define the condition to detect] + + +Delayed evaluation offset:: If a data source lags or is intermittent, you may supply +an optional value to evaluate alert conditions following a fixed delay. For instance, if data +is consistently indexed 5-10 minutes following its original timestamp, a *Delayed evaluation +offset* of `10 minutes` would ensure that alertable instances are still captured. +Index (entity):: This clause requires an *index or index pattern*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. +By:: This clause specifies the field to use in the previously provided +*index or index pattern* for tracking Entities. An entity is a `keyword` +or `number` field that consistently identifies the entity to be tracked. +When entity:: This clause specifies which crossing option to track. The values +*Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions +should trigger an alert. *Entered* alerts on entry into a boundary, *Exited* alerts on exit +from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances +or exits. +Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_shape` field* +identifying boundaries, and an optional *Human-readable boundary name* for better alerting +messages. + +[float] +[[alert-type-tracking-containment]] +=== Tracking containment +The Tracking containment alert type runs an {es} query over indices, determining if any +documents are currently contained within any boundaries from the specified boundary index. +In the event that an entity is contained within a boundary, an alert may be generated. + +[float] +==== Defining the conditions +Tracking containment alerts have 3 clauses that define the condition to detect, +as well as 2 Kuery bars used to provide additional filtering context for each of the indices. + +[role="screenshot"] +image::images/alert-types-tracking-containment-conditions.png[Five clauses define the condition to detect] + +Index (entity):: This clause requires an *index or index pattern*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. +When entity:: This clause specifies which crossing option to track. The values +*Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions +should trigger an alert. *Entered* alerts on entry into a boundary, *Exited* alerts on exit +from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances +or exits. +Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_shape` field* +identifying boundaries, and an optional *Human-readable boundary name* for better alerting +messages. + +Conditions for how an alert is tracked can be specified uniquely for each individual action. +An alert can be triggered either when a containment condition is met or when an entity +is no longer contained. + +[role="screenshot"] +image::images/alert-types-tracking-containment-action-options.png[Five clauses define the condition to detect] diff --git a/docs/user/alerting/images/alert-types-tracking-containment-action-options.png b/docs/user/alerting/images/alert-types-tracking-containment-action-options.png new file mode 100644 index 00000000000000..c0a045f8273829 Binary files /dev/null and b/docs/user/alerting/images/alert-types-tracking-containment-action-options.png differ diff --git a/docs/user/alerting/images/alert-types-tracking-containment-conditions.png b/docs/user/alerting/images/alert-types-tracking-containment-conditions.png new file mode 100644 index 00000000000000..32c17d2245d23c Binary files /dev/null and b/docs/user/alerting/images/alert-types-tracking-containment-conditions.png differ diff --git a/docs/user/alerting/images/alert-types-tracking-select.png b/docs/user/alerting/images/alert-types-tracking-select.png new file mode 100644 index 00000000000000..445a5202ffd0c2 Binary files /dev/null and b/docs/user/alerting/images/alert-types-tracking-select.png differ diff --git a/docs/user/alerting/images/alert-types-tracking-threshold-conditions.png b/docs/user/alerting/images/alert-types-tracking-threshold-conditions.png new file mode 100644 index 00000000000000..e51ce3e4b855f7 Binary files /dev/null and b/docs/user/alerting/images/alert-types-tracking-threshold-conditions.png differ diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index caef0c6e7332d6..25e87801f84af6 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -2,4 +2,5 @@ include::alerting-getting-started.asciidoc[] include::defining-alerts.asciidoc[] include::action-types.asciidoc[] include::alert-types.asciidoc[] +include::geo-alert-types.asciidoc[] include::alerting-production-considerations.asciidoc[] diff --git a/examples/expressions_explorer/README.md b/examples/expressions_explorer/README.md new file mode 100644 index 00000000000000..ead0ca758f8e5c --- /dev/null +++ b/examples/expressions_explorer/README.md @@ -0,0 +1,8 @@ +## expressions explorer + +This example expressions explorer app shows how to: + - to run expression + - to render expression output + - emit events from expression renderer and handle them + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/expressions_explorer/kibana.json b/examples/expressions_explorer/kibana.json new file mode 100644 index 00000000000000..038b7eea0ef21e --- /dev/null +++ b/examples/expressions_explorer/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "expressionsExplorer", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["expressions", "inspector", "uiActions", "developerExamples"], + "optionalPlugins": [], + "requiredBundles": [] +} diff --git a/examples/expressions_explorer/public/actions/navigate_action.ts b/examples/expressions_explorer/public/actions/navigate_action.ts new file mode 100644 index 00000000000000..d29a9e6b345b67 --- /dev/null +++ b/examples/expressions_explorer/public/actions/navigate_action.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { createAction } from '../../../../src/plugins/ui_actions/public'; + +export const ACTION_NAVIGATE = 'ACTION_NAVIGATE'; + +export const createNavigateAction = () => + createAction({ + id: ACTION_NAVIGATE, + type: ACTION_NAVIGATE, + getDisplayName: () => 'Navigate', + execute: async (event: any) => { + window.location.href = event.href; + }, + }); diff --git a/examples/state_containers_examples/server/types.ts b/examples/expressions_explorer/public/actions/navigate_trigger.ts similarity index 61% rename from examples/state_containers_examples/server/types.ts rename to examples/expressions_explorer/public/actions/navigate_trigger.ts index 86dc8d556e4c1c..eacbd968eaa93a 100644 --- a/examples/state_containers_examples/server/types.ts +++ b/examples/expressions_explorer/public/actions/navigate_trigger.ts @@ -6,7 +6,10 @@ * Public License, v 1. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface StateDemoPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface StateDemoPluginStart {} +import { Trigger } from '../../../../src/plugins/ui_actions/public'; + +export const NAVIGATE_TRIGGER_ID = 'NAVIGATE_TRIGGER_ID'; + +export const navigateTrigger: Trigger = { + id: NAVIGATE_TRIGGER_ID, +}; diff --git a/examples/expressions_explorer/public/actions_and_expressions.tsx b/examples/expressions_explorer/public/actions_and_expressions.tsx new file mode 100644 index 00000000000000..6e2eebcde4a0f7 --- /dev/null +++ b/examples/expressions_explorer/public/actions_and_expressions.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPanel, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { + ExpressionsStart, + ReactExpressionRenderer, + ExpressionsInspectorAdapter, +} from '../../../src/plugins/expressions/public'; +import { ExpressionEditor } from './editor/expression_editor'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { NAVIGATE_TRIGGER_ID } from './actions/navigate_trigger'; + +interface Props { + expressions: ExpressionsStart; + actions: UiActionsStart; +} + +export function ActionsExpressionsExample({ expressions, actions }: Props) { + const [expression, updateExpression] = useState( + 'button name="click me" href="http://www.google.com"' + ); + + const expressionChanged = (value: string) => { + updateExpression(value); + }; + + const inspectorAdapters = { + expression: new ExpressionsInspectorAdapter(), + }; + + const handleEvents = (event: any) => { + if (event.id !== 'NAVIGATE') return; + // enrich event context with some extra data + event.baseUrl = 'http://www.google.com'; + + actions.executeTriggerActions(NAVIGATE_TRIGGER_ID, event.value); + }; + + return ( + + + + +

Actions from expression renderers

+
+
+
+ + + + + + Here you can play with sample `button` which takes a url as configuration and + displays a button which emits custom BUTTON_CLICK trigger to which we have attached + a custom action which performs the navigation. + + + + + + + + + + + + + { + return
{message}
; + }} + /> +
+
+
+
+
+
+ ); +} diff --git a/examples/expressions_explorer/public/app.tsx b/examples/expressions_explorer/public/app.tsx new file mode 100644 index 00000000000000..d72cf08128a5ae --- /dev/null +++ b/examples/expressions_explorer/public/app.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { + EuiPage, + EuiPageHeader, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiSpacer, + EuiText, + EuiLink, +} from '@elastic/eui'; +import { AppMountParameters } from '../../../src/core/public'; +import { ExpressionsStart } from '../../../src/plugins/expressions/public'; +import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; +import { RunExpressionsExample } from './run_expressions'; +import { RenderExpressionsExample } from './render_expressions'; +import { ActionsExpressionsExample } from './actions_and_expressions'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; + +interface Props { + expressions: ExpressionsStart; + inspector: InspectorStart; + actions: UiActionsStart; +} + +const ExpressionsExplorer = ({ expressions, inspector, actions }: Props) => { + return ( + + + Expressions Explorer + + + +

+ There are a couple of ways to run the expressions. Below some of the options are + demonstrated. You can read more about it{' '} + + here + +

+
+ + + + + + + + + + + + +
+
+
+
+ ); +}; + +export const renderApp = (props: Props, { element }: AppMountParameters) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/examples/expressions_explorer/public/editor/expression_editor.tsx b/examples/expressions_explorer/public/editor/expression_editor.tsx new file mode 100644 index 00000000000000..e3dbb5998b92eb --- /dev/null +++ b/examples/expressions_explorer/public/editor/expression_editor.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { EuiCodeEditor } from '@elastic/eui'; + +interface Props { + value: string; + onChange: (value: string) => void; +} + +export function ExpressionEditor({ value, onChange }: Props) { + return ( + {}} + aria-label="Code Editor" + /> + ); +} diff --git a/examples/expressions_explorer/public/functions/button.ts b/examples/expressions_explorer/public/functions/button.ts new file mode 100644 index 00000000000000..8c39aa2743b305 --- /dev/null +++ b/examples/expressions_explorer/public/functions/button.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../src/plugins/expressions/common'; + +interface Arguments { + href: string; + name: string; +} + +export type ExpressionFunctionButton = ExpressionFunctionDefinition< + 'button', + unknown, + Arguments, + unknown +>; + +export const buttonFn: ExpressionFunctionButton = { + name: 'button', + args: { + href: { + help: i18n.translate('expressions.functions.font.args.href', { + defaultMessage: 'Link to which to navigate', + }), + types: ['string'], + required: true, + }, + name: { + help: i18n.translate('expressions.functions.font.args.name', { + defaultMessage: 'Name of the button', + }), + types: ['string'], + default: 'button', + }, + }, + help: 'Configures the button', + fn: (input: unknown, args: Arguments) => { + return { + type: 'render', + as: 'button', + value: args, + }; + }, +}; diff --git a/examples/state_containers_examples/common/index.ts b/examples/expressions_explorer/public/index.ts similarity index 70% rename from examples/state_containers_examples/common/index.ts rename to examples/expressions_explorer/public/index.ts index 0d0bc48fca450b..a6dbbc9198f44d 100644 --- a/examples/state_containers_examples/common/index.ts +++ b/examples/expressions_explorer/public/index.ts @@ -6,5 +6,6 @@ * Public License, v 1. */ -export const PLUGIN_ID = 'stateContainersExampleWithDataServices'; -export const PLUGIN_NAME = 'State containers example - with data services'; +import { ExpressionsExplorerPlugin } from './plugin'; + +export const plugin = () => new ExpressionsExplorerPlugin(); diff --git a/examples/expressions_explorer/public/inspector/ast_debug_view.tsx b/examples/expressions_explorer/public/inspector/ast_debug_view.tsx new file mode 100644 index 00000000000000..d860ff30bd8e99 --- /dev/null +++ b/examples/expressions_explorer/public/inspector/ast_debug_view.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState } from 'react'; +import { EuiTreeView, EuiDescriptionList, EuiCodeBlock, EuiText, EuiSpacer } from '@elastic/eui'; + +interface Props { + ast: any; +} + +const decorateAst = (ast: any, nodeClicked: any) => { + return ast.chain.map((link: any) => { + return { + id: link.function + Math.random(), + label: link.function, + callback: () => { + nodeClicked(link.debug); + }, + children: Object.keys(link.arguments).reduce((result: any, key: string) => { + if (typeof link.arguments[key] === 'object') { + // result[key] = decorateAst(link.arguments[key]); + } + return result; + }, []), + }; + }); +}; + +const prepareNode = (key: string, value: any) => { + if (key === 'args') { + return ( + + {JSON.stringify(value, null, '\t')} + + ); + } else if (key === 'output' || key === 'input') { + return ( + + {JSON.stringify(value, null, '\t')} + + ); + } else if (key === 'success') { + return value ? 'true' : 'false'; + } else return {value}; +}; + +export function AstDebugView({ ast }: Props) { + const [nodeInfo, setNodeInfo] = useState([] as any[]); + const items = decorateAst(ast, (node: any) => { + setNodeInfo( + Object.keys(node).map((key) => ({ + title: key, + description: prepareNode(key, node[key]), + })) + ); + }); + + return ( +
+ List of executed expression functions: + + + Details of selected function: + +
+ ); +} diff --git a/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx b/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx new file mode 100644 index 00000000000000..1233735072d04b --- /dev/null +++ b/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { InspectorViewProps, Adapters } from '../../../../src/plugins/inspector/public'; +import { AstDebugView } from './ast_debug_view'; + +interface ExpressionsInspectorViewComponentState { + ast: any; + adapters: Adapters; +} + +class ExpressionsInspectorViewComponent extends Component< + InspectorViewProps, + ExpressionsInspectorViewComponentState +> { + static propTypes = { + adapters: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + }; + + state = {} as ExpressionsInspectorViewComponentState; + + static getDerivedStateFromProps( + nextProps: Readonly, + state: ExpressionsInspectorViewComponentState + ) { + if (state && nextProps.adapters === state.adapters) { + return null; + } + + const { ast } = nextProps.adapters.expression; + + return { + adapters: nextProps.adapters, + ast, + }; + } + + onUpdateData = (ast: any) => { + this.setState({ + ast, + }); + }; + + componentDidMount() { + this.props.adapters.expression!.on('change', this.onUpdateData); + } + + componentWillUnmount() { + this.props.adapters.expression!.removeListener('change', this.onUpdateData); + } + + static renderNoData() { + return ( + + + + } + body={ + +

+ +

+
+ } + /> + ); + } + + render() { + if (!this.state.ast) { + return ExpressionsInspectorViewComponent.renderNoData(); + } + + return ; + } +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default ExpressionsInspectorViewComponent; diff --git a/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx b/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx new file mode 100644 index 00000000000000..b10c82e5df309d --- /dev/null +++ b/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { lazy } from 'react'; + +const ExpressionsInspectorViewComponent = lazy(() => import('./expressions_inspector_view')); + +export const getExpressionsInspectorViewComponentWrapper = () => { + return (props: any) => { + return ; + }; +}; diff --git a/examples/expressions_explorer/public/inspector/index.ts b/examples/expressions_explorer/public/inspector/index.ts new file mode 100644 index 00000000000000..ec87a1240ac747 --- /dev/null +++ b/examples/expressions_explorer/public/inspector/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { Adapters, InspectorViewDescription } from '../../../../src/plugins/inspector/public'; +import { getExpressionsInspectorViewComponentWrapper } from './expressions_inspector_view_wrapper'; + +export const getExpressionsInspectorViewDescription = (): InspectorViewDescription => ({ + title: i18n.translate('data.inspector.table.dataTitle', { + defaultMessage: 'Expression', + }), + order: 100, + help: i18n.translate('data.inspector.table..dataDescriptionTooltip', { + defaultMessage: 'View the expression behind the visualization', + }), + shouldShow(adapters: Adapters) { + return Boolean(adapters.expression); + }, + component: getExpressionsInspectorViewComponentWrapper(), +}); diff --git a/examples/expressions_explorer/public/plugin.tsx b/examples/expressions_explorer/public/plugin.tsx new file mode 100644 index 00000000000000..9643389ad881c1 --- /dev/null +++ b/examples/expressions_explorer/public/plugin.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public'; +import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import { ExpressionsSetup, ExpressionsStart } from '../../../src/plugins/expressions/public'; +import { + Setup as InspectorSetup, + Start as InspectorStart, +} from '../../../src/plugins/inspector/public'; +import { getExpressionsInspectorViewDescription } from './inspector'; +import { UiActionsStart, UiActionsSetup } from '../../../src/plugins/ui_actions/public'; +import { NAVIGATE_TRIGGER_ID, navigateTrigger } from './actions/navigate_trigger'; +import { ACTION_NAVIGATE, createNavigateAction } from './actions/navigate_action'; +import { buttonRenderer } from './renderers/button'; +import { buttonFn } from './functions/button'; + +interface StartDeps { + expressions: ExpressionsStart; + inspector: InspectorStart; + uiActions: UiActionsStart; +} + +interface SetupDeps { + uiActions: UiActionsSetup; + expressions: ExpressionsSetup; + inspector: InspectorSetup; + developerExamples: DeveloperExamplesSetup; +} + +export class ExpressionsExplorerPlugin implements Plugin { + public setup(core: CoreSetup, deps: SetupDeps) { + // register custom inspector adapter & view + deps.inspector.registerView(getExpressionsInspectorViewDescription()); + + // register custom actions + deps.uiActions.registerTrigger(navigateTrigger); + deps.uiActions.registerAction(createNavigateAction()); + deps.uiActions.attachAction(NAVIGATE_TRIGGER_ID, ACTION_NAVIGATE); + + // register custom functions and renderers + deps.expressions.registerRenderer(buttonRenderer); + deps.expressions.registerFunction(buttonFn); + + core.application.register({ + id: 'expressionsExplorer', + title: 'Expressions Explorer', + navLinkStatus: AppNavLinkStatus.hidden, + async mount(params: AppMountParameters) { + const [, depsStart] = await core.getStartServices(); + const { renderApp } = await import('./app'); + return renderApp( + { + expressions: depsStart.expressions, + inspector: depsStart.inspector, + actions: depsStart.uiActions, + }, + params + ); + }, + }); + + deps.developerExamples.register({ + appId: 'expressionsExplorer', + title: 'Expressions', + description: `Expressions is a plugin that allows to execute Kibana expressions and render content using expression renderers. This example plugin showcases various usage scenarios.`, + links: [ + { + label: 'README', + href: 'https://github.com/elastic/kibana/blob/master/src/plugins/expressions/README.md', + iconType: 'logoGithub', + size: 's', + target: '_blank', + }, + ], + }); + } + + public start() {} + + public stop() {} +} diff --git a/examples/expressions_explorer/public/render_expressions.tsx b/examples/expressions_explorer/public/render_expressions.tsx new file mode 100644 index 00000000000000..ffbe558f30218e --- /dev/null +++ b/examples/expressions_explorer/public/render_expressions.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPanel, + EuiText, + EuiTitle, + EuiButton, +} from '@elastic/eui'; +import { + ExpressionsStart, + ReactExpressionRenderer, + ExpressionsInspectorAdapter, +} from '../../../src/plugins/expressions/public'; +import { ExpressionEditor } from './editor/expression_editor'; +import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; + +interface Props { + expressions: ExpressionsStart; + inspector: InspectorStart; +} + +export function RenderExpressionsExample({ expressions, inspector }: Props) { + const [expression, updateExpression] = useState('markdown "## expressions explorer rendering"'); + + const expressionChanged = (value: string) => { + updateExpression(value); + }; + + const inspectorAdapters = { + expression: new ExpressionsInspectorAdapter(), + }; + + return ( + + + + +

Render expressions

+
+
+
+ + + + + + In the below editor you can enter your expression and render it. Using + ReactExpressionRenderer component makes that very easy. + + + + { + inspector.open(inspectorAdapters); + }} + > + Open Inspector + + + + + + + + + + + + + { + return
{message}
; + }} + /> +
+
+
+
+
+
+ ); +} diff --git a/examples/expressions_explorer/public/renderers/button.tsx b/examples/expressions_explorer/public/renderers/button.tsx new file mode 100644 index 00000000000000..32f1f31894dced --- /dev/null +++ b/examples/expressions_explorer/public/renderers/button.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import ReactDOM from 'react-dom'; +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { ExpressionRenderDefinition } from '../../../../src/plugins/expressions/common/expression_renderers'; + +export const buttonRenderer: ExpressionRenderDefinition = { + name: 'button', + displayName: 'Button', + reuseDomNode: true, + render(domNode, config, handlers) { + const buttonClick = () => { + handlers.event({ + id: 'NAVIGATE', + value: { + href: config.href, + }, + }); + }; + + const renderDebug = () => ( +
+ + {config.name} + +
+ ); + + ReactDOM.render(renderDebug(), domNode, () => handlers.done()); + + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); + }, +}; diff --git a/examples/expressions_explorer/public/run_expressions.tsx b/examples/expressions_explorer/public/run_expressions.tsx new file mode 100644 index 00000000000000..efbdbc2d418364 --- /dev/null +++ b/examples/expressions_explorer/public/run_expressions.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState, useEffect, useMemo } from 'react'; +import { + EuiCodeBlock, + EuiFlexItem, + EuiFlexGroup, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPanel, + EuiText, + EuiTitle, + EuiButton, +} from '@elastic/eui'; +import { + ExpressionsStart, + ExpressionsInspectorAdapter, +} from '../../../src/plugins/expressions/public'; +import { ExpressionEditor } from './editor/expression_editor'; +import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; + +interface Props { + expressions: ExpressionsStart; + inspector: InspectorStart; +} + +export function RunExpressionsExample({ expressions, inspector }: Props) { + const [expression, updateExpression] = useState('markdown "## expressions explorer"'); + const [result, updateResult] = useState({}); + + const expressionChanged = (value: string) => { + updateExpression(value); + }; + + const inspectorAdapters = useMemo( + () => ({ + expression: new ExpressionsInspectorAdapter(), + }), + [] + ); + + useEffect(() => { + const runExpression = async () => { + const execution = expressions.execute(expression, null, { + debug: true, + inspectorAdapters, + }); + + const data: any = await execution.getData(); + updateResult(data); + }; + + runExpression(); + }, [expression, expressions, inspectorAdapters]); + + return ( + + + + +

Run expressions

+
+
+
+ + + + + + In the below editor you can enter your expression and execute it. Using + expressions.execute allows you to easily run the expression. + + + + { + inspector.open(inspectorAdapters); + }} + > + Open Inspector + + + + + + + + + + + + + + {JSON.stringify(result, null, '\t')} + + + + + + +
+ ); +} diff --git a/examples/expressions_explorer/tsconfig.json b/examples/expressions_explorer/tsconfig.json new file mode 100644 index 00000000000000..b4449819b25a62 --- /dev/null +++ b/examples/expressions_explorer/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + ] +} diff --git a/examples/state_containers_examples/README.md b/examples/state_containers_examples/README.md index c4c6642789bd9d..015959a2f78199 100644 --- a/examples/state_containers_examples/README.md +++ b/examples/state_containers_examples/README.md @@ -2,7 +2,7 @@ This example app shows how to: - Use state containers to manage your application state - - Integrate with browser history and hash history routing + - Integrate with browser history or hash history routing - Sync your state container with the URL To run this example, use the command `yarn start --run-examples`. diff --git a/examples/state_containers_examples/kibana.json b/examples/state_containers_examples/kibana.json index 58346af8f1d191..0f0a3a805ecb5d 100644 --- a/examples/state_containers_examples/kibana.json +++ b/examples/state_containers_examples/kibana.json @@ -2,9 +2,9 @@ "id": "stateContainersExamples", "version": "0.0.1", "kibanaVersion": "kibana", - "server": true, + "server": false, "ui": true, "requiredPlugins": ["navigation", "data", "developerExamples"], "optionalPlugins": [], - "requiredBundles": ["kibanaUtils", "kibanaReact"] + "requiredBundles": ["kibanaUtils"] } diff --git a/examples/state_containers_examples/public/common/example_page.tsx b/examples/state_containers_examples/public/common/example_page.tsx new file mode 100644 index 00000000000000..203b226158d0e1 --- /dev/null +++ b/examples/state_containers_examples/public/common/example_page.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { PropsWithChildren } from 'react'; +import { EuiPage, EuiPageSideBar, EuiSideNav } from '@elastic/eui'; +import { CoreStart } from '../../../../src/core/public'; + +export interface ExampleLink { + title: string; + appId: string; +} + +interface NavProps { + navigateToApp: CoreStart['application']['navigateToApp']; + exampleLinks: ExampleLink[]; +} + +const SideNav: React.FC = ({ navigateToApp, exampleLinks }: NavProps) => { + const navItems = exampleLinks.map((example) => ({ + id: example.appId, + name: example.title, + onClick: () => navigateToApp(example.appId), + 'data-test-subj': example.appId, + })); + + return ( + + ); +}; + +interface Props { + navigateToApp: CoreStart['application']['navigateToApp']; + exampleLinks: ExampleLink[]; +} + +export const StateContainersExamplesPage: React.FC = ({ + navigateToApp, + children, + exampleLinks, +}: PropsWithChildren) => { + return ( + + + + + {children} + + ); +}; diff --git a/examples/state_containers_examples/public/plugin.ts b/examples/state_containers_examples/public/plugin.ts index 752c0935c5dd03..a775c3d65fd7a2 100644 --- a/examples/state_containers_examples/public/plugin.ts +++ b/examples/state_containers_examples/public/plugin.ts @@ -8,8 +8,8 @@ import { AppMountParameters, CoreSetup, Plugin, AppNavLinkStatus } from '../../../src/core/public'; import { AppPluginDependencies } from './with_data_services/types'; -import { PLUGIN_ID, PLUGIN_NAME } from '../common'; import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import image from './state_sync.png'; interface SetupDeps { developerExamples: DeveloperExamplesSetup; @@ -17,97 +17,95 @@ interface SetupDeps { export class StateContainersExamplesPlugin implements Plugin { public setup(core: CoreSetup, { developerExamples }: SetupDeps) { + const examples = { + stateContainersExampleBrowserHistory: { + title: 'Todo App (browser history)', + }, + stateContainersExampleHashHistory: { + title: 'Todo App (hash history)', + }, + stateContainersExampleWithDataServices: { + title: 'Search bar integration', + }, + }; + + const exampleLinks = Object.keys(examples).map((id: string) => ({ + appId: id, + title: examples[id as keyof typeof examples].title, + })); + core.application.register({ id: 'stateContainersExampleBrowserHistory', - title: 'State containers example - browser history routing', + title: examples.stateContainersExampleBrowserHistory.title, navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { const { renderApp, History } = await import('./todo/app'); - return renderApp(params, { - appInstanceId: '1', - appTitle: 'Routing with browser history', - historyType: History.Browser, - }); + const [coreStart] = await core.getStartServices(); + return renderApp( + params, + { + appTitle: examples.stateContainersExampleBrowserHistory.title, + historyType: History.Browser, + }, + { navigateToApp: coreStart.application.navigateToApp, exampleLinks } + ); }, }); core.application.register({ id: 'stateContainersExampleHashHistory', - title: 'State containers example - hash history routing', + title: examples.stateContainersExampleHashHistory.title, navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { const { renderApp, History } = await import('./todo/app'); - return renderApp(params, { - appInstanceId: '2', - appTitle: 'Routing with hash history', - historyType: History.Hash, - }); + const [coreStart] = await core.getStartServices(); + return renderApp( + params, + { + appTitle: examples.stateContainersExampleHashHistory.title, + historyType: History.Hash, + }, + { navigateToApp: coreStart.application.navigateToApp, exampleLinks } + ); }, }); core.application.register({ - id: PLUGIN_ID, - title: PLUGIN_NAME, + id: 'stateContainersExampleWithDataServices', + title: examples.stateContainersExampleWithDataServices.title, navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { - // Load application bundle const { renderApp } = await import('./with_data_services/application'); - // Get start services as specified in kibana.json const [coreStart, depsStart] = await core.getStartServices(); - // Render the application - return renderApp(coreStart, depsStart as AppPluginDependencies, params); + return renderApp(coreStart, depsStart as AppPluginDependencies, params, { exampleLinks }); }, }); developerExamples.register({ - appId: 'stateContainersExampleBrowserHistory', - title: 'State containers using browser history', - description: `An example todo app that uses browser history and state container utilities like createStateContainerReactHelpers, - createStateContainer, createKbnUrlStateStorage, createSessionStorageStateStorage, - syncStates and getStateFromKbnUrl to keep state in sync with the URL. Change some parameters, navigate away and then back, and the - state should be preserved.`, + appId: exampleLinks[0].appId, + title: 'State Management', + description: 'Examples of using state containers and state syncing utils.', + image, links: [ { - label: 'README', + label: 'State containers README', href: - 'https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers/README.md', + 'https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers', iconType: 'logoGithub', size: 's', target: '_blank', }, - ], - }); - - developerExamples.register({ - appId: 'stateContainersExampleHashHistory', - title: 'State containers using hash history', - description: `An example todo app that uses hash history and state container utilities like createStateContainerReactHelpers, - createStateContainer, createKbnUrlStateStorage, createSessionStorageStateStorage, - syncStates and getStateFromKbnUrl to keep state in sync with the URL. Change some parameters, navigate away and then back, and the - state should be preserved.`, - links: [ { - label: 'README', + label: 'State sync utils README', href: - 'https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers/README.md', + 'https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync', iconType: 'logoGithub', size: 's', target: '_blank', }, - ], - }); - - developerExamples.register({ - appId: PLUGIN_ID, - title: 'Sync state from a query bar with the url', - description: `Shows how to use data.syncQueryStateWitUrl in combination with state container utilities from kibana_utils to - show a query bar that stores state in the url and is kept in sync. - `, - links: [ { - label: 'README', - href: - 'https://github.com/elastic/kibana/blob/master/src/plugins/data/public/query/state_sync/README.md', - iconType: 'logoGithub', + label: 'Kibana navigation best practices', + href: 'https://www.elastic.co/guide/en/kibana/master/kibana-navigation.html', + iconType: 'logoKibana', size: 's', target: '_blank', }, diff --git a/examples/state_containers_examples/public/state_sync.png b/examples/state_containers_examples/public/state_sync.png new file mode 100644 index 00000000000000..fc8eb0dc10f6a2 Binary files /dev/null and b/examples/state_containers_examples/public/state_sync.png differ diff --git a/examples/state_containers_examples/public/todo/app.tsx b/examples/state_containers_examples/public/todo/app.tsx index ff4d65009a3671..f43ace6acee229 100644 --- a/examples/state_containers_examples/public/todo/app.tsx +++ b/examples/state_containers_examples/public/todo/app.tsx @@ -6,14 +6,14 @@ * Public License, v 1. */ -import { AppMountParameters } from 'kibana/public'; +import { AppMountParameters, CoreStart } from 'kibana/public'; import ReactDOM from 'react-dom'; import React from 'react'; import { createHashHistory } from 'history'; import { TodoAppPage } from './todo'; +import { StateContainersExamplesPage, ExampleLink } from '../common/example_page'; export interface AppOptions { - appInstanceId: string; appTitle: string; historyType: History; } @@ -23,30 +23,21 @@ export enum History { Hash, } +export interface Deps { + navigateToApp: CoreStart['application']['navigateToApp']; + exampleLinks: ExampleLink[]; +} + export const renderApp = ( { appBasePath, element, history: platformHistory }: AppMountParameters, - { appInstanceId, appTitle, historyType }: AppOptions + { appTitle, historyType }: AppOptions, + { navigateToApp, exampleLinks }: Deps ) => { const history = historyType === History.Browser ? platformHistory : createHashHistory(); ReactDOM.render( - { - const stripTrailingSlash = (path: string) => - path.charAt(path.length - 1) === '/' ? path.substr(0, path.length - 1) : path; - const currentAppUrl = stripTrailingSlash(history.createHref(history.location)); - if (historyType === History.Browser) { - // browser history - return currentAppUrl === '' && !history.location.search && !history.location.hash; - } else { - // hashed history - return currentAppUrl === '#' && !history.location.search; - } - }} - />, + + + , element ); diff --git a/examples/state_containers_examples/public/todo/todo.tsx b/examples/state_containers_examples/public/todo/todo.tsx index ba0b7d213f9fd2..efe45f15c809bc 100644 --- a/examples/state_containers_examples/public/todo/todo.tsx +++ b/examples/state_containers_examples/public/todo/todo.tsx @@ -6,7 +6,7 @@ * Public License, v 1. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { Link, Route, Router, Switch, useLocation } from 'react-router-dom'; import { History } from 'history'; import { @@ -18,21 +18,21 @@ import { EuiPageContentBody, EuiPageHeader, EuiPageHeaderSection, + EuiSpacer, + EuiText, EuiTitle, } from '@elastic/eui'; import { + BaseState, BaseStateContainer, - INullableBaseStateContainer, createKbnUrlStateStorage, - createSessionStorageStateStorage, createStateContainer, - createStateContainerReactHelpers, - PureTransition, - syncStates, getStateFromKbnUrl, - BaseState, + INullableBaseStateContainer, + StateContainer, + syncState, + useContainerSelector, } from '../../../../src/plugins/kibana_utils/public'; -import { useUrlTracker } from '../../../../src/plugins/kibana_react/public'; import { defaultState, pureTransitions, @@ -40,42 +40,24 @@ import { TodoState, } from '../../../../src/plugins/kibana_utils/demos/state_containers/todomvc'; -interface GlobalState { - text: string; -} -interface GlobalStateAction { - setText: PureTransition; -} -const defaultGlobalState: GlobalState = { text: '' }; -const globalStateContainer = createStateContainer( - defaultGlobalState, - { - setText: (state) => (text) => ({ ...state, text }), - } -); - -const GlobalStateHelpers = createStateContainerReactHelpers(); - -const container = createStateContainer(defaultState, pureTransitions); -const { Provider, connect, useTransitions, useState } = createStateContainerReactHelpers< - typeof container ->(); - interface TodoAppProps { filter: 'completed' | 'not-completed' | null; + stateContainer: StateContainer; } -const TodoApp: React.FC = ({ filter }) => { - const { setText } = GlobalStateHelpers.useTransitions(); - const { text } = GlobalStateHelpers.useState(); - const { edit: editTodo, delete: deleteTodo, add: addTodo } = useTransitions(); - const todos = useState().todos; - const filteredTodos = todos.filter((todo) => { - if (!filter) return true; - if (filter === 'completed') return todo.completed; - if (filter === 'not-completed') return !todo.completed; - return true; - }); +const TodoApp: React.FC = ({ filter, stateContainer }) => { + const { edit: editTodo, delete: deleteTodo, add: addTodo } = stateContainer.transitions; + const todos = useContainerSelector(stateContainer, (state) => state.todos); + const filteredTodos = useMemo( + () => + todos.filter((todo) => { + if (!filter) return true; + if (filter === 'completed') return todo.completed; + if (filter === 'not-completed') return !todo.completed; + return true; + }), + [todos, filter] + ); const location = useLocation(); return ( <> @@ -144,158 +126,115 @@ const TodoApp: React.FC = ({ filter }) => { > -
- - setText(e.target.value)} /> -
); }; -const TodoAppConnected = GlobalStateHelpers.connect(() => ({}))( - connect(() => ({}))(TodoApp) -); - export const TodoAppPage: React.FC<{ history: History; - appInstanceId: string; appTitle: string; appBasePath: string; - isInitialRoute: () => boolean; }> = (props) => { const initialAppUrl = React.useRef(window.location.href); - const [useHashedUrl, setUseHashedUrl] = React.useState(false); + const stateContainer = React.useMemo( + () => createStateContainer(defaultState, pureTransitions), + [] + ); - /** - * Replicates what src/legacy/ui/public/chrome/api/nav.ts did - * Persists the url in sessionStorage and tries to restore it on "componentDidMount" - */ - useUrlTracker(`lastUrlTracker:${props.appInstanceId}`, props.history, (urlToRestore) => { - // shouldRestoreUrl: - // App decides if it should restore url or not - // In this specific case, restore only if navigated to initial route - if (props.isInitialRoute()) { - // navigated to the base path, so should restore the url - return true; - } else { - // navigated to specific route, so should not restore the url - return false; - } - }); + // Most of kibana apps persist state in the URL in two ways: + // * Rison encoded. + // * Hashed URL: In the URL only the hash from the state is stored. The state itself is stored in + // the sessionStorage. See `state:storeInSessionStorage` advanced option for more context. + // This example shows how to use both of them + const [useHashedUrl, setUseHashedUrl] = React.useState(false); useEffect(() => { - // have to sync with history passed to react-router - // history v5 will be singleton and this will not be needed + // storage to sync our app state with + // in this case we want to sync state with query params in the URL serialised in rison format + // similar like Discover or Dashboard apps do const kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: useHashedUrl, history: props.history, }); - const sessionStorageStateStorage = createSessionStorageStateStorage(); - - /** - * Restoring global state: - * State restoration similar to what GlobalState in legacy world did - * It restores state both from url and from session storage - */ - const globalStateKey = `_g`; - const globalStateFromInitialUrl = getStateFromKbnUrl( - globalStateKey, - initialAppUrl.current - ); - const globalStateFromCurrentUrl = kbnUrlStateStorage.get(globalStateKey); - const globalStateFromSessionStorage = sessionStorageStateStorage.get( - globalStateKey - ); + // key to store state in the storage. In this case in the key of the query param in the URL + const appStateKey = `_todo`; - const initialGlobalState: GlobalState = { - ...defaultGlobalState, - ...globalStateFromCurrentUrl, - ...globalStateFromSessionStorage, - ...globalStateFromInitialUrl, - }; - globalStateContainer.set(initialGlobalState); - kbnUrlStateStorage.set(globalStateKey, initialGlobalState, { replace: true }); - sessionStorageStateStorage.set(globalStateKey, initialGlobalState); - - /** - * Restoring app local state: - * State restoration similar to what AppState in legacy world did - * It restores state both from url - */ - const appStateKey = `_todo-${props.appInstanceId}`; + // take care of initial state. Make sure state in memory is the same as in the URL before starting any syncing const initialAppState: TodoState = getStateFromKbnUrl(appStateKey, initialAppUrl.current) || kbnUrlStateStorage.get(appStateKey) || defaultState; - container.set(initialAppState); + stateContainer.set(initialAppState); kbnUrlStateStorage.set(appStateKey, initialAppState, { replace: true }); - // start syncing only when made sure, that state in synced - const { stop, start } = syncStates([ - { - stateContainer: withDefaultState(container, defaultState), - storageKey: appStateKey, - stateStorage: kbnUrlStateStorage, - }, - { - stateContainer: withDefaultState(globalStateContainer, defaultGlobalState), - storageKey: globalStateKey, - stateStorage: kbnUrlStateStorage, - }, - { - stateContainer: withDefaultState(globalStateContainer, defaultGlobalState), - storageKey: globalStateKey, - stateStorage: sessionStorageStateStorage, - }, - ]); + // start syncing state between state container and the URL + const { stop, start } = syncState({ + stateContainer: withDefaultState(stateContainer, defaultState), + storageKey: appStateKey, + stateStorage: kbnUrlStateStorage, + }); start(); return () => { stop(); - - // reset state containers - container.set(defaultState); - globalStateContainer.set(defaultGlobalState); }; - }, [props.appInstanceId, props.history, useHashedUrl]); + }, [stateContainer, props.history, useHashedUrl]); return ( - - - - - - -

- State sync example. Instance: ${props.appInstanceId}. {props.appTitle} -

-
- setUseHashedUrl(!useHashedUrl)}> - {useHashedUrl ? 'Use Expanded State' : 'Use Hashed State'} - -
-
- - - - - - - - - - - - - - - -
-
-
+ + + + +

{props.appTitle}

+
+ + +

+ This is a simple TODO app that uses state containers and state syncing utils. It + stores state in the URL similar like Discover or Dashboard apps do.
+ Play with the app and see how the state is persisted in the URL. +
Undo/Redo with browser history also works. +

+
+
+
+ + + + + + + + + + + + + + + +

Most of kibana apps persist state in the URL in two ways:

+
    +
  1. Expanded state in rison format
  2. +
  3. + Just a state hash.
    + In the URL only the hash from the state is stored. The state itself is stored in + the sessionStorage. See `state:storeInSessionStorage` advanced option for more + context. +
  4. +
+

You can switch between these two mods:

+
+ + setUseHashedUrl(!useHashedUrl)}> + {useHashedUrl ? 'Use Expanded State' : 'Use Hashed State'} + +
+
+
); }; diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/app.tsx similarity index 58% rename from examples/state_containers_examples/public/with_data_services/components/app.tsx rename to examples/state_containers_examples/public/with_data_services/app.tsx index b526032a5becb9..fc84e1e952aaa9 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/app.tsx @@ -6,50 +6,47 @@ * Public License, v 1. */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { History } from 'history'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { Router } from 'react-router-dom'; import { EuiFieldText, - EuiPage, EuiPageBody, EuiPageContent, EuiPageHeader, + EuiText, EuiTitle, } from '@elastic/eui'; +import { CoreStart } from 'kibana/public'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; -import { CoreStart } from '../../../../../src/core/public'; -import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; import { connectToQueryState, - syncQueryStateWithUrl, DataPublicPluginStart, - IIndexPattern, - QueryState, - Filter, esFilters, + Filter, + IIndexPattern, Query, -} from '../../../../../src/plugins/data/public'; + QueryState, + syncQueryStateWithUrl, +} from '../../../../src/plugins/data/public'; import { - BaseState, BaseStateContainer, createStateContainer, - createStateContainerReactHelpers, IKbnUrlStateStorage, - ReduxLikeStateContainer, syncState, -} from '../../../../../src/plugins/kibana_utils/public'; -import { PLUGIN_ID, PLUGIN_NAME } from '../../../common'; + useContainerState, +} from '../../../../src/plugins/kibana_utils/public'; +import { ExampleLink, StateContainersExamplesPage } from '../common/example_page'; interface StateDemoAppDeps { - notifications: CoreStart['notifications']; - http: CoreStart['http']; + navigateToApp: CoreStart['application']['navigateToApp']; navigation: NavigationPublicPluginStart; data: DataPublicPluginStart; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; + exampleLinks: ExampleLink[]; } interface AppState { @@ -61,85 +58,74 @@ const defaultAppState: AppState = { name: '', filters: [], }; -const { - Provider: AppStateContainerProvider, - useState: useAppState, - useContainer: useAppStateContainer, -} = createStateContainerReactHelpers>(); -const App = ({ navigation, data, history, kbnUrlStateStorage }: StateDemoAppDeps) => { - const appStateContainer = useAppStateContainer(); - const appState = useAppState(); +export const App = ({ + navigation, + data, + history, + kbnUrlStateStorage, + exampleLinks, + navigateToApp, +}: StateDemoAppDeps) => { + const appStateContainer = useMemo(() => createStateContainer(defaultAppState), []); + const appState = useContainerState(appStateContainer); useGlobalStateSyncing(data.query, kbnUrlStateStorage); useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage); const indexPattern = useIndexPattern(data); if (!indexPattern) - return
No index pattern found. Please create an index patter before loading...
; + return ( +
+ No index pattern found. Please create an index pattern before trying this example... +
+ ); - // Render the application DOM. // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. return ( - - + + <> - - - - - -

- -

-
-
- - appStateContainer.set({ ...appState, name: e.target.value })} - aria-label="My name" - /> - -
-
+ + + +

Integration with search bar

+
+
+ +

+ This examples shows how you can use state containers, state syncing utils and + helpers from data plugin to sync your app state and search bar state with the URL. +

+
+ + + + +

+ In addition to state from query bar also sync your arbitrary application state: +

+
+ appStateContainer.set({ ...appState, name: e.target.value })} + aria-label="My name" + /> +
+
-
-
- ); -}; - -export const StateDemoApp = (props: StateDemoAppDeps) => { - const appStateContainer = useCreateStateContainer(defaultAppState); - - return ( - - - + + ); }; -function useCreateStateContainer( - defaultState: State -): ReduxLikeStateContainer { - const stateContainerRef = useRef | null>(null); - if (!stateContainerRef.current) { - stateContainerRef.current = createStateContainer(defaultState); - } - return stateContainerRef.current; -} - function useIndexPattern(data: DataPublicPluginStart) { const [indexPattern, setIndexPattern] = useState(); useEffect(() => { diff --git a/examples/state_containers_examples/public/with_data_services/application.tsx b/examples/state_containers_examples/public/with_data_services/application.tsx index d50c203a2a0797..4235446dd06e05 100644 --- a/examples/state_containers_examples/public/with_data_services/application.tsx +++ b/examples/state_containers_examples/public/with_data_services/application.tsx @@ -10,24 +10,26 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { AppMountParameters, CoreStart } from '../../../../src/core/public'; import { AppPluginDependencies } from './types'; -import { StateDemoApp } from './components/app'; +import { App } from './app'; import { createKbnUrlStateStorage } from '../../../../src/plugins/kibana_utils/public/'; +import { ExampleLink } from '../common/example_page'; export const renderApp = ( - { notifications, http }: CoreStart, + { notifications, application }: CoreStart, { navigation, data }: AppPluginDependencies, - { element, history }: AppMountParameters + { element, history }: AppMountParameters, + { exampleLinks }: { exampleLinks: ExampleLink[] } ) => { const kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history }); ReactDOM.render( - , element ); diff --git a/examples/state_containers_examples/server/plugin.ts b/examples/state_containers_examples/server/plugin.ts deleted file mode 100644 index 04ab4d7a0fede8..00000000000000 --- a/examples/state_containers_examples/server/plugin.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, - Logger, -} from '../../../src/core/server'; - -import { StateDemoPluginSetup, StateDemoPluginStart } from './types'; -import { defineRoutes } from './routes'; - -export class StateDemoServerPlugin implements Plugin { - private readonly logger: Logger; - - constructor(initializerContext: PluginInitializerContext) { - this.logger = initializerContext.logger.get(); - } - - public setup(core: CoreSetup) { - this.logger.debug('State_demo: Ssetup'); - const router = core.http.createRouter(); - - // Register server side APIs - defineRoutes(router); - - return {}; - } - - public start(core: CoreStart) { - this.logger.debug('State_demo: Started'); - return {}; - } - - public stop() {} -} - -export { StateDemoServerPlugin as Plugin }; diff --git a/examples/state_containers_examples/server/routes/index.ts b/examples/state_containers_examples/server/routes/index.ts deleted file mode 100644 index f7c7a6abe88086..00000000000000 --- a/examples/state_containers_examples/server/routes/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import { IRouter } from '../../../../src/core/server'; - -export function defineRoutes(router: IRouter) { - router.get( - { - path: '/api/state_demo/example', - validate: false, - }, - async (context, request, response) => { - return response.ok({ - body: { - time: new Date().toISOString(), - }, - }); - } - ); -} diff --git a/package.json b/package.json index ff6df054be220a..dac83dacf6fbfc 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", "@elastic/ems-client": "7.11.0", - "@elastic/eui": "31.0.0", + "@elastic/eui": "31.3.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", @@ -287,7 +287,7 @@ "react-resizable": "^1.7.5", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", - "react-use": "^13.27.0", + "react-use": "^15.3.4", "recompose": "^0.26.0", "redux": "^4.0.5", "redux-actions": "^2.6.5", @@ -592,6 +592,7 @@ "base64-js": "^1.3.1", "base64url": "^3.0.1", "broadcast-channel": "^3.0.3", + "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", "chromedriver": "^87.0.3", @@ -827,7 +828,7 @@ "url-loader": "^2.2.0", "use-resize-observer": "^6.0.0", "val-loader": "^1.1.1", - "vega": "^5.18.0", + "vega": "^5.19.1", "vega-lite": "^4.17.0", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.25.0", diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 4681057ad09980..f23d8c99369800 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -108,4 +108,4 @@ exports[`prepares assets for distribution: baz bundle 1`] = ` exports[`prepares assets for distribution: foo async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}}]);"`; -exports[`prepares assets for distribution: foo bundle 1`] = `"(function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i { log.indent(-2); }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts index a138673d69ebf2..2a238cdeb53854 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts @@ -12,7 +12,32 @@ import { decorateSnapshotUi, expectSnapshot } from './decorate_snapshot_ui'; import path from 'path'; import fs from 'fs'; +const createMockTest = ({ + title = 'Test', + passed = true, +}: { title?: string; passed?: boolean } = {}) => { + return { + fullTitle: () => title, + isPassed: () => passed, + parent: {}, + } as Test; +}; + describe('decorateSnapshotUi', () => { + const snapshotFolder = path.resolve(__dirname, '__snapshots__'); + const snapshotFile = path.resolve(snapshotFolder, 'decorate_snapshot_ui.test.snap'); + + const cleanup = () => { + if (fs.existsSync(snapshotFile)) { + fs.unlinkSync(snapshotFile); + fs.rmdirSync(snapshotFolder); + } + }; + + beforeEach(cleanup); + + afterAll(cleanup); + describe('when running a test', () => { let lifecycle: Lifecycle; beforeEach(() => { @@ -21,15 +46,7 @@ describe('decorateSnapshotUi', () => { }); it('passes when the snapshot matches the actual value', async () => { - const test: Test = { - title: 'Test', - file: 'foo.ts', - parent: { - file: 'foo.ts', - tests: [], - suites: [], - }, - } as any; + const test = createMockTest(); await lifecycle.beforeEachTest.trigger(test); @@ -39,15 +56,7 @@ describe('decorateSnapshotUi', () => { }); it('throws when the snapshot does not match the actual value', async () => { - const test: Test = { - title: 'Test', - file: 'foo.ts', - parent: { - file: 'foo.ts', - tests: [], - suites: [], - }, - } as any; + const test = createMockTest(); await lifecycle.beforeEachTest.trigger(test); @@ -57,27 +66,10 @@ describe('decorateSnapshotUi', () => { }); it('writes a snapshot to an external file if it does not exist', async () => { - const test: Test = { - title: 'Test', - file: __filename, - isPassed: () => true, - } as any; - - // @ts-expect-error - test.parent = { - file: __filename, - tests: [test], - suites: [], - }; + const test: Test = createMockTest(); await lifecycle.beforeEachTest.trigger(test); - const snapshotFile = path.resolve( - __dirname, - '__snapshots__', - 'decorate_snapshot_ui.test.snap' - ); - expect(fs.existsSync(snapshotFile)).toBe(false); expect(() => { @@ -87,10 +79,48 @@ describe('decorateSnapshotUi', () => { await lifecycle.afterTestSuite.trigger(test.parent); expect(fs.existsSync(snapshotFile)).toBe(true); + }); + }); - fs.unlinkSync(snapshotFile); + describe('when writing multiple snapshots to a single file', () => { + let lifecycle: Lifecycle; + beforeEach(() => { + lifecycle = new Lifecycle(); + decorateSnapshotUi({ lifecycle, updateSnapshots: false, isCi: false }); + }); + + beforeEach(() => { + fs.mkdirSync(path.resolve(__dirname, '__snapshots__')); + fs.writeFileSync( + snapshotFile, + `// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[\`Test1 1\`] = \`"foo"\`; + +exports[\`Test2 1\`] = \`"bar"\`; + `, + { encoding: 'utf-8' } + ); + }); + + it('compares to an existing snapshot', async () => { + const test1 = createMockTest({ title: 'Test1' }); + + await lifecycle.beforeEachTest.trigger(test1); + + expect(() => { + expectSnapshot('foo').toMatch(); + }).not.toThrow(); + + const test2 = createMockTest({ title: 'Test2' }); - fs.rmdirSync(path.resolve(__dirname, '__snapshots__')); + await lifecycle.beforeEachTest.trigger(test2); + + expect(() => { + expectSnapshot('foo').toMatch(); + }).toThrow(); + + await lifecycle.afterTestSuite.trigger(test1.parent); }); }); @@ -102,15 +132,7 @@ describe('decorateSnapshotUi', () => { }); it("doesn't throw if the value does not match", async () => { - const test: Test = { - title: 'Test', - file: 'foo.ts', - parent: { - file: 'foo.ts', - tests: [], - suites: [], - }, - } as any; + const test = createMockTest(); await lifecycle.beforeEachTest.trigger(test); @@ -128,15 +150,7 @@ describe('decorateSnapshotUi', () => { }); it('throws on new snapshots', async () => { - const test: Test = { - title: 'Test', - file: 'foo.ts', - parent: { - file: 'foo.ts', - tests: [], - suites: [], - }, - } as any; + const test = createMockTest(); await lifecycle.beforeEachTest.trigger(test); @@ -144,5 +158,82 @@ describe('decorateSnapshotUi', () => { expectSnapshot('bar').toMatchInline(); }).toThrow(); }); + + describe('when adding to an existing file', () => { + beforeEach(() => { + fs.mkdirSync(path.resolve(__dirname, '__snapshots__')); + fs.writeFileSync( + snapshotFile, + `// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[\`Test 1\`] = \`"foo"\`; + +exports[\`Test2 1\`] = \`"bar"\`; + `, + { encoding: 'utf-8' } + ); + }); + + it('does not throw on an existing test', async () => { + const test = createMockTest({ title: 'Test' }); + + await lifecycle.beforeEachTest.trigger(test); + + expect(() => { + expectSnapshot('foo').toMatch(); + }).not.toThrow(); + }); + + it('throws on a new test', async () => { + const test = createMockTest({ title: 'New test' }); + + await lifecycle.beforeEachTest.trigger(test); + + expect(() => { + expectSnapshot('foo').toMatch(); + }).toThrow(); + }); + + it('does not throw when all snapshots are used ', async () => { + const test = createMockTest({ title: 'Test' }); + + await lifecycle.beforeEachTest.trigger(test); + + expect(() => { + expectSnapshot('foo').toMatch(); + }).not.toThrow(); + + const test2 = createMockTest({ title: 'Test2' }); + + await lifecycle.beforeEachTest.trigger(test2); + + expect(() => { + expectSnapshot('bar').toMatch(); + }).not.toThrow(); + + const afterTestSuite = lifecycle.afterTestSuite.trigger({}); + + await expect(afterTestSuite).resolves.toBe(undefined); + }); + + it('throws on unused snapshots', async () => { + const test = createMockTest({ title: 'Test' }); + + await lifecycle.beforeEachTest.trigger(test); + + expect(() => { + expectSnapshot('foo').toMatch(); + }).not.toThrow(); + + const afterTestSuite = lifecycle.afterTestSuite.trigger({}); + + await expect(afterTestSuite).rejects.toMatchInlineSnapshot(` + [Error: 1 obsolete snapshot(s) found: + Test2 1. + + Run tests again with \`--updateSnapshots\` to remove them.] + `); + }); + }); }); }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts index c43b50de3afd01..2111f1a6e5e900 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts @@ -15,9 +15,10 @@ import { import path from 'path'; import prettier from 'prettier'; import babelTraverse from '@babel/traverse'; -import { flatten, once } from 'lodash'; +import { once } from 'lodash'; +import callsites from 'callsites'; import { Lifecycle } from '../lifecycle'; -import { Test, Suite } from '../../fake_mocha_types'; +import { Test } from '../../fake_mocha_types'; type ISnapshotState = InstanceType; @@ -28,40 +29,17 @@ interface SnapshotContext { currentTestName: string; } -let testContext: { - file: string; - snapshotTitle: string; - snapshotContext: SnapshotContext; -} | null = null; - -let registered: boolean = false; - -function getSnapshotMeta(currentTest: Test) { - // Make sure snapshot title is unique per-file, rather than entire - // suite. This allows reuse of tests, for instance to compare - // results for different configurations. - - const titles = [currentTest.title]; - const file = currentTest.file; - - let test: Suite | undefined = currentTest?.parent; - - while (test && test.file === file) { - titles.push(test.title); - test = test.parent; - } - - const snapshotTitle = titles.reverse().join(' '); - - if (!file || !snapshotTitle) { - throw new Error(`file or snapshotTitle not available in Mocha test context`); - } - - return { - file, - snapshotTitle, - }; -} +const globalState: { + updateSnapshot: SnapshotUpdateState; + registered: boolean; + currentTest: Test | null; + snapshots: Array<{ tests: Test[]; file: string; snapshotState: ISnapshotState }>; +} = { + updateSnapshot: 'none', + registered: false, + currentTest: null, + snapshots: [], +}; const modifyStackTracePrepareOnce = once(() => { const originalPrepareStackTrace = Error.prepareStackTrace; @@ -72,7 +50,7 @@ const modifyStackTracePrepareOnce = once(() => { Error.prepareStackTrace = (error, structuredStackTrace) => { let filteredStrackTrace: NodeJS.CallSite[] = structuredStackTrace; - if (registered) { + if (globalState.registered) { filteredStrackTrace = filteredStrackTrace.filter((callSite) => { // check for both compiled and uncompiled files return !callSite.getFileName()?.match(/decorate_snapshot_ui\.(js|ts)/); @@ -94,21 +72,16 @@ export function decorateSnapshotUi({ updateSnapshots: boolean; isCi: boolean; }) { - let snapshotStatesByFilePath: Record< - string, - { snapshotState: ISnapshotState; testsInFile: Test[] } - > = {}; - - registered = true; - - let updateSnapshot: SnapshotUpdateState; + globalState.registered = true; + globalState.snapshots.length = 0; + globalState.currentTest = null; if (isCi) { // make sure snapshots that have not been committed // are not written to file on CI, passing the test - updateSnapshot = 'none'; + globalState.updateSnapshot = 'none'; } else { - updateSnapshot = updateSnapshots ? 'all' : 'new'; + globalState.updateSnapshot = updateSnapshots ? 'all' : 'new'; } modifyStackTracePrepareOnce(); @@ -125,21 +98,8 @@ export function decorateSnapshotUi({ // @ts-expect-error global.expectSnapshot = expectSnapshot; - lifecycle.beforeEachTest.add((currentTest: Test) => { - const { file, snapshotTitle } = getSnapshotMeta(currentTest); - - if (!snapshotStatesByFilePath[file]) { - snapshotStatesByFilePath[file] = getSnapshotState(file, currentTest, updateSnapshot); - } - - testContext = { - file, - snapshotTitle, - snapshotContext: { - snapshotState: snapshotStatesByFilePath[file].snapshotState, - currentTestName: snapshotTitle, - }, - }; + lifecycle.beforeEachTest.add((test: Test) => { + globalState.currentTest = test; }); lifecycle.afterTestSuite.add(function (testSuite) { @@ -150,19 +110,18 @@ export function decorateSnapshotUi({ const unused: string[] = []; - Object.keys(snapshotStatesByFilePath).forEach((file) => { - const { snapshotState, testsInFile } = snapshotStatesByFilePath[file]; - - testsInFile.forEach((test) => { - const snapshotMeta = getSnapshotMeta(test); + globalState.snapshots.forEach((snapshot) => { + const { tests, snapshotState } = snapshot; + tests.forEach((test) => { + const title = test.fullTitle(); // If test is failed or skipped, mark snapshots as used. Otherwise, // running a test in isolation will generate false positives. if (!test.isPassed()) { - snapshotState.markSnapshotsAsCheckedForTest(snapshotMeta.snapshotTitle); + snapshotState.markSnapshotsAsCheckedForTest(title); } }); - if (!updateSnapshots) { + if (globalState.updateSnapshot !== 'all') { unused.push(...snapshotState.getUncheckedKeys()); } else { snapshotState.removeUncheckedKeys(); @@ -179,28 +138,14 @@ export function decorateSnapshotUi({ ); } - snapshotStatesByFilePath = {}; + globalState.snapshots.length = 0; }); } -function recursivelyGetTestsFromSuite(suite: Suite): Test[] { - return suite.tests.concat(flatten(suite.suites.map((s) => recursivelyGetTestsFromSuite(s)))); -} - -function getSnapshotState(file: string, test: Test, updateSnapshot: SnapshotUpdateState) { +function getSnapshotState(file: string, updateSnapshot: SnapshotUpdateState) { const dirname = path.dirname(file); const filename = path.basename(file); - let parent: Suite | undefined = test.parent; - - while (parent && parent.parent?.file === file) { - parent = parent.parent; - } - - if (!parent) { - throw new Error('Top-level suite not found'); - } - const snapshotState = new SnapshotState( path.join(dirname + `/__snapshots__/` + filename.replace(path.extname(filename), '.snap')), { @@ -211,24 +156,54 @@ function getSnapshotState(file: string, test: Test, updateSnapshot: SnapshotUpda } ); - return { snapshotState, testsInFile: recursivelyGetTestsFromSuite(parent) }; + return snapshotState; } export function expectSnapshot(received: any) { - if (!registered) { + if (!globalState.registered) { throw new Error( 'Mocha hooks were not registered before expectSnapshot was used. Call `registerMochaHooksForSnapshots` in your top-level describe().' ); } - if (!testContext) { - throw new Error('A current Mocha context is needed to match snapshots'); + if (!globalState.currentTest) { + throw new Error('expectSnapshot can only be called inside of an it()'); + } + + const [, fileOfTest] = callsites().map((site) => site.getFileName()); + + if (!fileOfTest) { + throw new Error("Couldn't infer a filename for the current test"); + } + + let snapshot = globalState.snapshots.find(({ file }) => file === fileOfTest); + + if (!snapshot) { + snapshot = { + file: fileOfTest, + tests: [], + snapshotState: getSnapshotState(fileOfTest, globalState.updateSnapshot), + }; + globalState.snapshots.unshift(snapshot!); + } + + if (!snapshot) { + throw new Error('Snapshot is undefined'); + } + + if (!snapshot.tests.includes(globalState.currentTest)) { + snapshot.tests.push(globalState.currentTest); } + const context: SnapshotContext = { + snapshotState: snapshot.snapshotState, + currentTestName: globalState.currentTest.fullTitle(), + }; + return { - toMatch: expectToMatchSnapshot.bind(null, testContext.snapshotContext, received), + toMatch: expectToMatchSnapshot.bind(null, context, received), // use bind to support optional 3rd argument (actual) - toMatchInline: expectToMatchInlineSnapshot.bind(null, testContext.snapshotContext, received), + toMatchInline: expectToMatchInlineSnapshot.bind(null, context, received), }; } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 1a69c7db35a73c..b82254e5a14166 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -182,7 +182,12 @@ export class DocLinksService { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, }, monitoring: { + alertsCluster: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/cluster-alerts.html`, alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, + alertsKibanaCpuThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cpu-threshold`, + alertsKibanaDiskThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-disk-usage-threshold`, + alertsKibanaJvmThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-jvm-memory-threshold`, + alertsKibanaMissingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`, monitorElasticsearch: `${ELASTICSEARCH_DOCS}configuring-metricbeat.html`, monitorKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`, }, diff --git a/src/core/server/saved_objects/migrations/core/call_cluster.ts b/src/core/server/saved_objects/migrations/core/call_cluster.ts index ba6027f711b62a..2cb1656833728b 100644 --- a/src/core/server/saved_objects/migrations/core/call_cluster.ts +++ b/src/core/server/saved_objects/migrations/core/call_cluster.ts @@ -18,7 +18,7 @@ export interface CallCluster { (path: 'bulk', opts: { body: object[] }): Promise; (path: 'count', opts: CountOpts): Promise<{ count: number; _shards: ShardsInfo }>; (path: 'clearScroll', opts: { scrollId: string }): Promise; - (path: 'indices.create' | 'indices.delete', opts: IndexCreationOpts): Promise; + (path: 'indices.create', opts: IndexCreationOpts): Promise; (path: 'indices.exists', opts: IndexOpts): Promise; (path: 'indices.existsAlias', opts: { name: string }): Promise; (path: 'indices.get', opts: IndexOpts & Ignorable): Promise; diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 741f715ba6ebe6..6ba652abda3d59 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -206,6 +206,20 @@ describe('DocumentMigrator', () => { ); }); + it('coerces the current Kibana version if it has a hyphen', () => { + const validDefinition = { + kibanaVersion: '3.2.0-SNAPSHOT', + typeRegistry: createRegistry({ + name: 'foo', + convertToMultiNamespaceTypeVersion: '3.2.0', + namespaceType: 'multiple', + }), + minimumConvertVersion: '0.0.0', + log: mockLogger, + }; + expect(() => new DocumentMigrator(validDefinition)).not.toThrowError(); + }); + it('validates convertToMultiNamespaceTypeVersion is not used on a patch version', () => { const invalidDefinition = { kibanaVersion: '3.2.3', diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index e4b89a949d3cf7..e93586ec7ce4c2 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -159,10 +159,11 @@ export class DocumentMigrator implements VersionedTransformer { */ constructor({ typeRegistry, - kibanaVersion, + kibanaVersion: rawKibanaVersion, minimumConvertVersion = DEFAULT_MINIMUM_CONVERT_VERSION, log, }: DocumentMigratorOptions) { + const kibanaVersion = rawKibanaVersion.split('-')[0]; // coerce a semver-like string (x.y.z-SNAPSHOT) or prerelease version (x.y.z-alpha) to a regular semver (x.y.z) validateMigrationDefinition(typeRegistry, kibanaVersion, minimumConvertVersion); this.documentMigratorOptions = { typeRegistry, kibanaVersion, log }; diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts index 32ecea94826ff9..4b35b017a7063e 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts @@ -107,17 +107,6 @@ describe('ElasticIndex', () => { }); }); - describe('deleteIndex', () => { - test('calls indices.delete', async () => { - await Index.deleteIndex(client, '.lotr'); - - expect(client.indices.delete).toHaveBeenCalledTimes(1); - expect(client.indices.delete).toHaveBeenCalledWith({ - index: '.lotr', - }); - }); - }); - describe('claimAlias', () => { test('handles unaliased indices', async () => { client.indices.getAlias.mockResolvedValue( diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index 9cdec926a56bae..aa7802320dfb72 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -221,10 +221,6 @@ export async function createIndex( }); } -export async function deleteIndex(client: MigrationEsClient, index: string) { - await client.indices.delete({ index }); -} - /** * Converts an index to an alias. The `alias` parameter is the desired alias name which currently * is a concrete index. This function will reindex `alias` into a new index, delete the `alias` diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.ts index 2653e96e1b8d5c..c4bb19d8223fff 100644 --- a/src/core/server/saved_objects/migrations/core/migration_es_client.ts +++ b/src/core/server/saved_objects/migrations/core/migration_es_client.ts @@ -20,7 +20,6 @@ const methods = [ 'clearScroll', 'count', 'indices.create', - 'indices.delete', 'indices.deleteTemplate', 'indices.get', 'indices.getAlias', diff --git a/src/core/server/ui_settings/integration_tests/routes.test.ts b/src/core/server/ui_settings/integration_tests/routes.test.ts index 89f5b6732a8fb2..2f0a5ebc139e67 100644 --- a/src/core/server/ui_settings/integration_tests/routes.test.ts +++ b/src/core/server/ui_settings/integration_tests/routes.test.ts @@ -9,7 +9,8 @@ import { schema } from '@kbn/config-schema'; import * as kbnTestServer from '../../../test_helpers/kbn_server'; -describe('ui settings service', () => { +// FLAKY: https://github.com/elastic/kibana/issues/89191 +describe.skip('ui settings service', () => { describe('routes', () => { let root: ReturnType; beforeAll(async () => { diff --git a/src/core/server/ui_settings/settings/navigation.test.ts b/src/core/server/ui_settings/settings/navigation.test.ts index 1ba81b4e79f46b..0e6fbdfdbdb6e2 100644 --- a/src/core/server/ui_settings/settings/navigation.test.ts +++ b/src/core/server/ui_settings/settings/navigation.test.ts @@ -28,18 +28,4 @@ describe('navigation settings', () => { ); }); }); - - describe('pageNavigation', () => { - const validate = getValidationFn(navigationSettings.pageNavigation); - - it('should only accept valid values', () => { - expect(() => validate('modern')).not.toThrow(); - expect(() => validate('legacy')).not.toThrow(); - expect(() => validate('invalid')).toThrowErrorMatchingInlineSnapshot(` -"types that failed validation: -- [0]: expected value to equal [modern] -- [1]: expected value to equal [legacy]" -`); - }); - }); }); diff --git a/src/core/server/ui_settings/settings/navigation.ts b/src/core/server/ui_settings/settings/navigation.ts index 38064db9e9388d..937af4bb9aad1c 100644 --- a/src/core/server/ui_settings/settings/navigation.ts +++ b/src/core/server/ui_settings/settings/navigation.ts @@ -37,25 +37,5 @@ export const getNavigationSettings = (): Record => { 'The route must be a relative URL.', }), }, - pageNavigation: { - name: i18n.translate('core.ui_settings.params.pageNavigationName', { - defaultMessage: 'Side nav style', - }), - value: 'modern', - description: i18n.translate('core.ui_settings.params.pageNavigationDesc', { - defaultMessage: 'Change the style of navigation', - }), - type: 'select', - options: ['modern', 'legacy'], - optionLabels: { - modern: i18n.translate('core.ui_settings.params.pageNavigationModern', { - defaultMessage: 'Modern', - }), - legacy: i18n.translate('core.ui_settings.params.pageNavigationLegacy', { - defaultMessage: 'Legacy', - }), - }, - schema: schema.oneOf([schema.literal('modern'), schema.literal('legacy')]), - }, }; }; diff --git a/src/plugins/charts/public/static/utils/transform_click_event.ts b/src/plugins/charts/public/static/utils/transform_click_event.ts index 20865bea2f897a..7c28db333cc830 100644 --- a/src/plugins/charts/public/static/utils/transform_click_event.ts +++ b/src/plugins/charts/public/static/utils/transform_click_event.ts @@ -30,6 +30,9 @@ export interface BrushTriggerEvent { type AllSeriesAccessors = Array<[accessor: Accessor | AccessorFn, value: string | number]>; +// TODO: replace when exported from elastic/charts +const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; + /** * returns accessor value from string or function accessor * @param datum @@ -82,6 +85,29 @@ const getAllSplitAccessors = ( value, ]); +/** + * Gets value from small multiple accessors + * + * Only handles single small multiple accessor + */ +function getSplitChartValue({ + smHorizontalAccessorValue, + smVerticalAccessorValue, +}: Pick): + | string + | number + | undefined { + if (smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) { + return smHorizontalAccessorValue; + } + + if (smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) { + return smVerticalAccessorValue; + } + + return; +} + /** * Reduces matching column indexes * @@ -92,7 +118,8 @@ const getAllSplitAccessors = ( const columnReducer = ( xAccessor: Accessor | AccessorFn | null, yAccessor: Accessor | AccessorFn | null, - splitAccessors: AllSeriesAccessors + splitAccessors: AllSeriesAccessors, + splitChartAccessor?: Accessor | AccessorFn ) => ( acc: Array<[index: number, id: string]>, { id }: Datatable['columns'][number], @@ -101,6 +128,7 @@ const columnReducer = ( if ( (xAccessor !== null && validateAccessorId(id, xAccessor)) || (yAccessor !== null && validateAccessorId(id, yAccessor)) || + (splitChartAccessor !== undefined && validateAccessorId(id, splitChartAccessor)) || splitAccessors.some(([accessor]) => validateAccessorId(id, accessor)) ) { acc.push([index, id]); @@ -121,13 +149,18 @@ const rowFindPredicate = ( geometry: GeometryValue | null, xAccessor: Accessor | AccessorFn | null, yAccessor: Accessor | AccessorFn | null, - splitAccessors: AllSeriesAccessors + splitAccessors: AllSeriesAccessors, + splitChartAccessor?: Accessor | AccessorFn, + splitChartValue?: string | number ) => (row: Datatable['rows'][number]): boolean => (geometry === null || (xAccessor !== null && getAccessorValue(row, xAccessor) === geometry.x && yAccessor !== null && - getAccessorValue(row, yAccessor) === geometry.y)) && + getAccessorValue(row, yAccessor) === geometry.y && + (splitChartAccessor === undefined || + (splitChartValue !== undefined && + getAccessorValue(row, splitChartAccessor) === splitChartValue)))) && [...splitAccessors].every(([accessor, value]) => getAccessorValue(row, accessor) === value); /** @@ -142,19 +175,28 @@ export const getFilterFromChartClickEventFn = ( table: Datatable, xAccessor: Accessor | AccessorFn, splitSeriesAccessorFnMap?: Map, + splitChartAccessor?: Accessor | AccessorFn, negate: boolean = false ) => (points: Array<[GeometryValue, XYChartSeriesIdentifier]>): ClickTriggerEvent => { const data: ValueClickContext['data']['data'] = []; points.forEach((point) => { const [geometry, { yAccessor, splitAccessors }] = point; + const splitChartValue = getSplitChartValue(point[1]); const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap); const columns = table.columns.reduce>( - columnReducer(xAccessor, yAccessor, allSplitAccessors), + columnReducer(xAccessor, yAccessor, allSplitAccessors, splitChartAccessor), [] ); const row = table.rows.findIndex( - rowFindPredicate(geometry, xAccessor, yAccessor, allSplitAccessors) + rowFindPredicate( + geometry, + xAccessor, + yAccessor, + allSplitAccessors, + splitChartAccessor, + splitChartValue + ) ); const newData = columns.map(([column, id]) => ({ table, @@ -179,16 +221,20 @@ export const getFilterFromChartClickEventFn = ( * Helper function to get filter action event from series */ export const getFilterFromSeriesFn = (table: Datatable) => ( - { splitAccessors }: XYChartSeriesIdentifier, + { splitAccessors, ...rest }: XYChartSeriesIdentifier, splitSeriesAccessorFnMap?: Map, + splitChartAccessor?: Accessor | AccessorFn, negate = false ): ClickTriggerEvent => { + const splitChartValue = getSplitChartValue(rest); const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap); const columns = table.columns.reduce>( - columnReducer(null, null, allSplitAccessors), + columnReducer(null, null, allSplitAccessors, splitChartAccessor), [] ); - const row = table.rows.findIndex(rowFindPredicate(null, null, null, allSplitAccessors)); + const row = table.rows.findIndex( + rowFindPredicate(null, null, null, allSplitAccessors, splitChartAccessor, splitChartValue) + ); const data: ValueClickContext['data']['data'] = columns.map(([column, id]) => ({ table, column, diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.test.ts b/src/plugins/dashboard/public/application/lib/session_restoration.test.ts new file mode 100644 index 00000000000000..56db5346b7c6c9 --- /dev/null +++ b/src/plugins/dashboard/public/application/lib/session_restoration.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { dataPluginMock } from '../../../../data/public/mocks'; +import { createSessionRestorationDataProvider } from './session_restoration'; +import { getAppStateDefaults } from './get_app_state_defaults'; +import { getSavedDashboardMock } from '../test_helpers'; +import { SavedObjectTagDecoratorTypeGuard } from '../../../../saved_objects_tagging_oss/public'; + +describe('createSessionRestorationDataProvider', () => { + const mockDataPlugin = dataPluginMock.createStartContract(); + const searchSessionInfoProvider = createSessionRestorationDataProvider({ + data: mockDataPlugin, + getAppState: () => + getAppStateDefaults( + getSavedDashboardMock(), + false, + ((() => false) as unknown) as SavedObjectTagDecoratorTypeGuard + ), + getDashboardTitle: () => 'Dashboard', + getDashboardId: () => 'Id', + }); + + describe('session state', () => { + test('restoreState has sessionId and initialState has not', async () => { + const searchSessionId = 'id'; + (mockDataPlugin.search.session.getSessionId as jest.Mock).mockImplementation( + () => searchSessionId + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.searchSessionId).toBeUndefined(); + expect(restoreState.searchSessionId).toBe(searchSessionId); + }); + + test('restoreState has absoluteTimeRange', async () => { + const relativeTime = 'relativeTime'; + const absoluteTime = 'absoluteTime'; + (mockDataPlugin.query.timefilter.timefilter.getTime as jest.Mock).mockImplementation( + () => relativeTime + ); + (mockDataPlugin.query.timefilter.timefilter.getAbsoluteTime as jest.Mock).mockImplementation( + () => absoluteTime + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.timeRange).toBe(relativeTime); + expect(restoreState.timeRange).toBe(absoluteTime); + }); + + test('restoreState has refreshInterval paused', async () => { + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.refreshInterval).toBeUndefined(); + expect(restoreState.refreshInterval?.pause).toBe(true); + }); + }); +}); diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.ts b/src/plugins/dashboard/public/application/lib/session_restoration.ts index 60a0c56a63218a..fb57f8caa5ce4e 100644 --- a/src/plugins/dashboard/public/application/lib/session_restoration.ts +++ b/src/plugins/dashboard/public/application/lib/session_restoration.ts @@ -21,8 +21,8 @@ export function createSessionRestorationDataProvider(deps: { getUrlGeneratorData: async () => { return { urlGeneratorId: DASHBOARD_APP_URL_GENERATOR, - initialState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: false }), - restoreState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: true }), + initialState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: false }), + restoreState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: true }), }; }, }; @@ -32,20 +32,17 @@ function getUrlGeneratorState({ data, getAppState, getDashboardId, - forceAbsoluteTime, + shouldRestoreSearchSession, }: { data: DataPublicPluginStart; getAppState: () => DashboardAppState; getDashboardId: () => string; - /** - * Can force time range from time filter to convert from relative to absolute time range - */ - forceAbsoluteTime: boolean; + shouldRestoreSearchSession: boolean; }): DashboardUrlGeneratorState { const appState = getAppState(); return { dashboardId: getDashboardId(), - timeRange: forceAbsoluteTime + timeRange: shouldRestoreSearchSession ? data.query.timefilter.timefilter.getAbsoluteTime() : data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -55,6 +52,12 @@ function getUrlGeneratorState({ preserveSavedFilters: false, viewMode: appState.viewMode, panels: getDashboardId() ? undefined : appState.panels, - searchSessionId: data.search.session.getSessionId(), + searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined, + refreshInterval: shouldRestoreSearchSession + ? { + pause: true, // force pause refresh interval when restoring a session + value: 0, + } + : undefined, }; } diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index 3e09fa449a1aa3..4ef61ec0f25571 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -57,9 +57,16 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": undefined, "lang": "lang", "name": "name", "readFromDocValues": false, + "runtimeField": Object { + "script": Object { + "source": "emit('hello world')", + }, + "type": "keyword", + }, "script": "script", "scripted": true, "searchable": true, diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts index bce75f9932479a..8a73abb3c7d830 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts @@ -9,7 +9,7 @@ import { IndexPatternField } from './index_pattern_field'; import { IndexPattern } from '../index_patterns'; import { KBN_FIELD_TYPES, FieldFormat } from '../../../common'; -import { FieldSpec } from '../types'; +import { FieldSpec, RuntimeField } from '../types'; describe('Field', function () { function flatten(obj: Record) { @@ -42,6 +42,12 @@ describe('Field', function () { } as unknown) as IndexPattern, $$spec: ({} as unknown) as FieldSpec, conflictDescriptions: { a: ['b', 'c'], d: ['e'] }, + runtimeField: { + type: 'keyword' as RuntimeField['type'], + script: { + source: "emit('hello world')", + }, + }, }; it('the correct properties are writable', () => { diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 540563c3a8cfc7..ed6c4bd40d5616 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -6,9 +6,10 @@ * Public License, v 1. */ +import type { RuntimeField } from '../types'; import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; -import { IFieldType } from './types'; +import type { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; import { shortenDottedString } from '../../utils'; @@ -35,6 +36,14 @@ export class IndexPatternField implements IFieldType { this.spec.count = count; } + public get runtimeField() { + return this.spec.runtimeField; + } + + public set runtimeField(runtimeField: RuntimeField | undefined) { + this.spec.runtimeField = runtimeField; + } + /** * Script field code */ @@ -117,6 +126,13 @@ export class IndexPatternField implements IFieldType { return this.spec.subType; } + /** + * Is the field part of the index mapping? + */ + public get isMapped() { + return this.spec.isMapped; + } + // not writable, not serialized public get sortable() { return ( @@ -181,6 +197,8 @@ export class IndexPatternField implements IFieldType { format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined, customLabel: this.customLabel, shortDotsEnable: this.spec.shortDotsEnable, + runtimeField: this.runtimeField, + isMapped: this.isMapped, }; } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 76de2b2662bb02..4aadddfad3b970 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -20,9 +20,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "@tags", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -44,9 +46,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "@timestamp", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -68,9 +72,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "_id", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -92,9 +98,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "_source", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -116,9 +124,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "_type", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -140,9 +150,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "area", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -164,9 +176,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "bytes", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -188,9 +202,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "custom_user_field", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -212,9 +228,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "extension", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -236,9 +254,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "extension.keyword", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -264,9 +284,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "geo.coordinates", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -288,9 +310,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "geo.src", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -312,9 +336,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "hashed", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -336,9 +362,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "ip", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -360,9 +388,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "machine.os", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -384,9 +414,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "machine.os.raw", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -412,9 +444,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "non-filterable", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": false, @@ -436,9 +470,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "non-sortable", "readFromDocValues": false, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": false, @@ -460,9 +496,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "phpmemory", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -484,9 +522,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "point", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -508,9 +548,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "request_body", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -518,6 +560,35 @@ Object { "subType": undefined, "type": "attachment", }, + "runtime_field": Object { + "aggregatable": false, + "conflictDescriptions": undefined, + "count": 0, + "customLabel": undefined, + "esTypes": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, + "isMapped": undefined, + "lang": undefined, + "name": "runtime_field", + "readFromDocValues": false, + "runtimeField": Object { + "script": Object { + "source": "emit('hello world')", + }, + "type": "keyword", + }, + "script": undefined, + "scripted": false, + "searchable": false, + "shortDotsEnable": false, + "subType": undefined, + "type": undefined, + }, "script date": Object { "aggregatable": true, "conflictDescriptions": undefined, @@ -532,9 +603,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": false, "lang": "painless", "name": "script date", "readFromDocValues": false, + "runtimeField": undefined, "script": "1234", "scripted": true, "searchable": true, @@ -556,9 +629,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": false, "lang": "expression", "name": "script murmur3", "readFromDocValues": false, + "runtimeField": undefined, "script": "1234", "scripted": true, "searchable": true, @@ -580,9 +655,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": false, "lang": "expression", "name": "script number", "readFromDocValues": false, + "runtimeField": undefined, "script": "1234", "scripted": true, "searchable": true, @@ -604,9 +681,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": false, "lang": "expression", "name": "script string", "readFromDocValues": false, + "runtimeField": undefined, "script": "'i am a string'", "scripted": true, "searchable": true, @@ -628,9 +707,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "ssl", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -652,9 +733,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "time", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -676,9 +759,11 @@ Object { "pattern": "$0,0.[00]", }, }, + "isMapped": true, "lang": undefined, "name": "utc_time", "readFromDocValues": true, + "runtimeField": undefined, "script": undefined, "scripted": false, "searchable": true, @@ -689,6 +774,14 @@ Object { }, "id": "test-pattern", "intervalName": undefined, + "runtimeFieldMap": Object { + "runtime_field": Object { + "script": Object { + "source": "emit('hello world')", + }, + "type": "keyword", + }, + }, "sourceFilters": undefined, "timeFieldName": "timestamp", "title": "title", diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap index bad74430b89668..d6da4adac81a4c 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap @@ -10,6 +10,7 @@ Object { "fields": Object {}, "id": "id", "intervalName": undefined, + "runtimeFieldMap": Object {}, "sourceFilters": Array [ Object { "value": "item1", diff --git a/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js b/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js index 3e81b9234ee644..2bcb8df34cf026 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js +++ b/src/plugins/data/common/index_patterns/index_patterns/fixtures/logstash_fields.js @@ -68,6 +68,7 @@ function stubbedLogstashFields() { lang, scripted, subType, + isMapped: !scripted, }; }); } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index bb7ed17f9e6086..4f6e83460aecf2 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -18,9 +18,27 @@ import { IndexPatternField } from '../fields'; import { fieldFormatsMock } from '../../field_formats/mocks'; import { FieldFormat } from '../..'; +import { RuntimeField } from '../types'; class MockFieldFormatter {} +const runtimeFieldScript = { + type: 'keyword' as RuntimeField['type'], + script: { + source: "emit('hello world')", + }, +}; + +const runtimeFieldMap = { + runtime_field: runtimeFieldScript, +}; + +const runtimeField = { + name: 'runtime_field', + runtimeField: runtimeFieldScript, + scripted: false, +}; + fieldFormatsMock.getInstance = jest.fn().mockImplementation(() => new MockFieldFormatter()) as any; // helper function to create index patterns @@ -32,7 +50,15 @@ function create(id: string) { } = stubbedSavedObjectIndexPattern(id); return new IndexPattern({ - spec: { id, type, version, timeFieldName, fields, title }, + spec: { + id, + type, + version, + timeFieldName, + fields: { ...fields, runtime_field: runtimeField }, + title, + runtimeFieldMap, + }, fieldFormats: fieldFormatsMock, shortDotsEnable: false, metaFields: [], @@ -53,6 +79,10 @@ describe('IndexPattern', () => { expect(indexPattern).toHaveProperty('getNonScriptedFields'); expect(indexPattern).toHaveProperty('addScriptedField'); expect(indexPattern).toHaveProperty('removeScriptedField'); + expect(indexPattern).toHaveProperty('addScriptedField'); + expect(indexPattern).toHaveProperty('removeScriptedField'); + expect(indexPattern).toHaveProperty('addRuntimeField'); + expect(indexPattern).toHaveProperty('removeRuntimeField'); // properties expect(indexPattern).toHaveProperty('fields'); @@ -65,6 +95,7 @@ describe('IndexPattern', () => { expect(indexPattern.fields[0]).toHaveProperty('filterable'); expect(indexPattern.fields[0]).toHaveProperty('sortable'); expect(indexPattern.fields[0]).toHaveProperty('scripted'); + expect(indexPattern.fields[0]).toHaveProperty('isMapped'); }); }); @@ -98,6 +129,12 @@ describe('IndexPattern', () => { expect(docValueFieldNames).toContain('utc_time'); }); + test('should return runtimeField', () => { + expect(indexPattern.getComputedFields().runtimeFields).toEqual({ + runtime_field: runtimeFieldScript, + }); + }); + test('should request date field doc values in date_time format', () => { const { docvalueFields } = indexPattern.getComputedFields(); const timestampField = docvalueFields.find((field) => field.field === '@timestamp'); @@ -117,6 +154,7 @@ describe('IndexPattern', () => { const notScriptedNames = mockLogStashFields() .filter((item: IndexPatternField) => item.scripted === false) .map((item: IndexPatternField) => item.name); + notScriptedNames.push('runtime_field'); const respNames = map(indexPattern.getNonScriptedFields(), 'name'); expect(respNames).toEqual(notScriptedNames); @@ -185,6 +223,52 @@ describe('IndexPattern', () => { }); }); + describe('addRuntimeField and removeRuntimeField', () => { + const runtime = { + type: 'keyword' as RuntimeField['type'], + script: { + source: "emit('hello world');", + }, + }; + + beforeEach(() => { + const formatter = { + toJSON: () => ({ id: 'bytes' }), + } as FieldFormat; + indexPattern.getFormatterForField = () => formatter; + }); + + test('add and remove runtime field to existing field', () => { + indexPattern.addRuntimeField('@tags', runtime); + expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ + '@tags': runtime, + runtime_field: runtimeField.runtimeField, + }); + expect(indexPattern.toSpec()!.fields!['@tags'].runtimeField).toEqual(runtime); + + indexPattern.removeRuntimeField('@tags'); + expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ + runtime_field: runtimeField.runtimeField, + }); + expect(indexPattern.toSpec()!.fields!['@tags'].runtimeField).toBeUndefined(); + }); + + test('add and remove runtime field as new field', () => { + indexPattern.addRuntimeField('new_field', runtime); + expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ + runtime_field: runtimeField.runtimeField, + new_field: runtime, + }); + expect(indexPattern.toSpec()!.fields!.new_field.runtimeField).toEqual(runtime); + + indexPattern.removeRuntimeField('new_field'); + expect(indexPattern.toSpec().runtimeFieldMap).toEqual({ + runtime_field: runtimeField.runtimeField, + }); + expect(indexPattern.toSpec()!.fields!.new_field).toBeUndefined(); + }); + }); + describe('getFormatterForField', () => { test('should return the default one for empty objects', () => { indexPattern.setFieldFormat('scriptedFieldWithEmptyFormatter', {}); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 452c663d96716c..144d38fe15909f 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -8,6 +8,7 @@ import _, { each, reject } from 'lodash'; import { FieldAttrs, FieldAttrSet } from '../..'; +import type { RuntimeField } from '../types'; import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; @@ -17,6 +18,7 @@ import { flattenHitWrapper } from './flatten_hit'; import { FieldFormatsStartCommon, FieldFormat } from '../../field_formats'; import { IndexPatternSpec, TypeMeta, SourceFilter, IndexPatternFieldMap } from '../types'; import { SerializedFieldFormat } from '../../../../expressions/common'; +import { castEsToKbnFieldTypeName } from '../../kbn_field_types'; interface IndexPatternDeps { spec?: IndexPatternSpec; @@ -74,6 +76,8 @@ export class IndexPattern implements IIndexPattern { private shortDotsEnable: boolean = false; private fieldFormats: FieldFormatsStartCommon; private fieldAttrs: FieldAttrs; + private runtimeFieldMap: Record; + /** * prevents errors when index pattern exists before indices */ @@ -115,6 +119,7 @@ export class IndexPattern implements IIndexPattern { this.fieldAttrs = spec.fieldAttrs || {}; this.intervalName = spec.intervalName; this.allowNoIndex = spec.allowNoIndex || false; + this.runtimeFieldMap = spec.runtimeFieldMap || {}; } /** @@ -160,7 +165,8 @@ export class IndexPattern implements IIndexPattern { return { storedFields: ['*'], scriptFields, - docvalueFields: [], + docvalueFields: [] as Array<{ field: string; format: string }>, + runtimeFields: {}, }; } @@ -192,6 +198,7 @@ export class IndexPattern implements IIndexPattern { storedFields: ['*'], scriptFields, docvalueFields, + runtimeFields: this.runtimeFieldMap, }; } @@ -210,6 +217,7 @@ export class IndexPattern implements IIndexPattern { typeMeta: this.typeMeta, type: this.type, fieldFormats: this.fieldFormatMap, + runtimeFieldMap: this.runtimeFieldMap, fieldAttrs: this.fieldAttrs, intervalName: this.intervalName, allowNoIndex: this.allowNoIndex, @@ -305,6 +313,7 @@ export class IndexPattern implements IIndexPattern { ? undefined : JSON.stringify(this.fieldFormatMap); const fieldAttrs = this.getFieldAttrs(); + const runtimeFieldMap = this.runtimeFieldMap; return { fieldAttrs: fieldAttrs ? JSON.stringify(fieldAttrs) : undefined, @@ -319,6 +328,7 @@ export class IndexPattern implements IIndexPattern { type: this.type, typeMeta: this.typeMeta ? JSON.stringify(this.typeMeta) : undefined, allowNoIndex: this.allowNoIndex ? this.allowNoIndex : undefined, + runtimeFieldMap: runtimeFieldMap ? JSON.stringify(runtimeFieldMap) : undefined, }; } @@ -340,6 +350,51 @@ export class IndexPattern implements IIndexPattern { ); } + /** + * Add a runtime field - Appended to existing mapped field or a new field is + * created as appropriate + * @param name Field name + * @param runtimeField Runtime field definition + */ + + addRuntimeField(name: string, runtimeField: RuntimeField) { + const existingField = this.getFieldByName(name); + if (existingField) { + existingField.runtimeField = runtimeField; + } else { + this.fields.add({ + name, + runtimeField, + type: castEsToKbnFieldTypeName(runtimeField.type), + aggregatable: true, + searchable: true, + count: 0, + readFromDocValues: false, + }); + } + this.runtimeFieldMap[name] = runtimeField; + } + + /** + * Remove a runtime field - removed from mapped field or removed unmapped + * field as appropriate + * @param name Field name + */ + + removeRuntimeField(name: string) { + const existingField = this.getFieldByName(name); + if (existingField) { + if (existingField.isMapped) { + // mapped field, remove runtimeField def + existingField.runtimeField = undefined; + } else { + // runtimeField only + this.fields.remove(existingField); + } + } + delete this.runtimeFieldMap[name]; + } + /** * Get formatter for a given field name. Return undefined if none exists * @param field diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 80cb8a55fa0a02..60436da530b636 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -11,6 +11,7 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsClientCommon } from '../..'; import { createIndexPatternCache } from '.'; +import type { RuntimeField } from '../types'; import { IndexPattern } from './index_pattern'; import { createEnsureDefaultIndexPattern, @@ -34,6 +35,7 @@ import { SavedObjectNotFound } from '../../../../kibana_utils/common'; import { IndexPatternMissingIndices } from '../lib'; import { findByTitle } from '../utils'; import { DuplicateIndexPatternError } from '../errors'; +import { castEsToKbnFieldTypeName } from '../../kbn_field_types'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const savedObjectType = 'index-pattern'; @@ -247,7 +249,8 @@ export class IndexPatternsService { */ refreshFields = async (indexPattern: IndexPattern) => { try { - const fields = await this.getFieldsForIndexPattern(indexPattern); + const fields = (await this.getFieldsForIndexPattern(indexPattern)) as FieldSpec[]; + fields.forEach((field) => (field.isMapped = true)); const scripted = indexPattern.getScriptedFields().map((field) => field.spec); const fieldAttrs = indexPattern.getFieldAttrs(); const fieldsWithSavedAttrs = Object.values( @@ -288,6 +291,7 @@ export class IndexPatternsService { try { let updatedFieldList: FieldSpec[]; const newFields = (await this.getFieldsForWildcard(options)) as FieldSpec[]; + newFields.forEach((field) => (field.isMapped = true)); // If allowNoIndex, only update field list if field caps finds fields. To support // beats creating index pattern and dashboard before docs @@ -347,6 +351,7 @@ export class IndexPatternsService { fields, sourceFilters, fieldFormatMap, + runtimeFieldMap, typeMeta, type, fieldAttrs, @@ -359,6 +364,9 @@ export class IndexPatternsService { const parsedFieldFormatMap = fieldFormatMap ? JSON.parse(fieldFormatMap) : {}; const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : []; const parsedFieldAttrs: FieldAttrs = fieldAttrs ? JSON.parse(fieldAttrs) : {}; + const parsedRuntimeFieldMap: Record = runtimeFieldMap + ? JSON.parse(runtimeFieldMap) + : {}; return { id, @@ -373,6 +381,7 @@ export class IndexPatternsService { fieldFormats: parsedFieldFormatMap, fieldAttrs: parsedFieldAttrs, allowNoIndex, + runtimeFieldMap: parsedRuntimeFieldMap, }; }; @@ -387,7 +396,7 @@ export class IndexPatternsService { } const spec = this.savedObjectToSpec(savedObject); - const { title, type, typeMeta } = spec; + const { title, type, typeMeta, runtimeFieldMap } = spec; spec.fieldAttrs = savedObject.attributes.fieldAttrs ? JSON.parse(savedObject.attributes.fieldAttrs) : {}; @@ -406,6 +415,22 @@ export class IndexPatternsService { }, spec.fieldAttrs ); + // APPLY RUNTIME FIELDS + for (const [key, value] of Object.entries(runtimeFieldMap || {})) { + if (spec.fields[key]) { + spec.fields[key].runtimeField = value; + } else { + spec.fields[key] = { + name: key, + type: castEsToKbnFieldTypeName(value.type), + runtimeField: value, + aggregatable: true, + searchable: true, + count: 0, + readFromDocValues: false, + }; + } + } } catch (err) { if (err instanceof IndexPatternMissingIndices) { this.onNotification({ diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 9f9a26604a0e56..467b5125f03271 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -14,6 +14,14 @@ import { SerializedFieldFormat } from '../../../expressions/common'; import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..'; export type FieldFormatMap = Record; +const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; +type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; +export interface RuntimeField { + type: RuntimeType; + script: { + source: string; + }; +} /** * IIndexPattern allows for an IndexPattern OR an index pattern saved object @@ -51,6 +59,7 @@ export interface IndexPatternAttributes { sourceFilters?: string; fieldFormatMap?: string; fieldAttrs?: string; + runtimeFieldMap?: string; /** * prevents errors when index pattern exists before indices */ @@ -199,8 +208,10 @@ export interface FieldSpec { subType?: IFieldSubType; indexed?: boolean; customLabel?: string; + runtimeField?: RuntimeField; // not persisted shortDotsEnable?: boolean; + isMapped?: boolean; } export type IndexPatternFieldMap = Record; @@ -230,6 +241,7 @@ export interface IndexPatternSpec { typeMeta?: TypeMeta; type?: string; fieldFormats?: Record; + runtimeFieldMap?: Record; fieldAttrs?: FieldAttrs; allowNoIndex?: boolean; } diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 0b9c60e94a1985..6d7654c6659f23 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -20,6 +20,7 @@ const getComputedFields = () => ({ storedFields: [], scriptFields: {}, docvalueFields: [], + runtimeFields: {}, }); const mockSource = { excludes: ['foo-*'] }; @@ -37,6 +38,13 @@ const indexPattern2 = ({ getSourceFiltering: () => mockSource2, } as unknown) as IndexPattern; +const runtimeFieldDef = { + type: 'keyword', + script: { + source: "emit('hello world')", + }, +}; + describe('SearchSource', () => { let mockSearchMethod: any; let searchSourceDependencies: SearchSourceDependencies; @@ -82,12 +90,14 @@ describe('SearchSource', () => { describe('computed fields handling', () => { test('still provides computed fields when no fields are specified', async () => { + const runtimeFields = { runtime_field: runtimeFieldDef }; searchSource.setField('index', ({ ...indexPattern, getComputedFields: () => ({ storedFields: ['hello'], scriptFields: { world: {} }, docvalueFields: ['@timestamp'], + runtimeFields, }), } as unknown) as IndexPattern); @@ -95,6 +105,7 @@ describe('SearchSource', () => { expect(request.stored_fields).toEqual(['hello']); expect(request.script_fields).toEqual({ world: {} }); expect(request.fields).toEqual(['@timestamp']); + expect(request.runtime_mappings).toEqual(runtimeFields); }); test('never includes docvalue_fields', async () => { @@ -390,15 +401,23 @@ describe('SearchSource', () => { }); test('filters request when a specific list of fields is provided with fieldsFromSource', async () => { + const runtimeFields = { runtime_field: runtimeFieldDef, runtime_field_b: runtimeFieldDef }; searchSource.setField('index', ({ ...indexPattern, getComputedFields: () => ({ storedFields: ['*'], scriptFields: { hello: {}, world: {} }, docvalueFields: ['@timestamp', 'date'], + runtimeFields, }), } as unknown) as IndexPattern); - searchSource.setField('fieldsFromSource', ['hello', '@timestamp', 'foo-a', 'bar']); + searchSource.setField('fieldsFromSource', [ + 'hello', + '@timestamp', + 'foo-a', + 'bar', + 'runtime_field', + ]); const request = await searchSource.getSearchRequestBody(); expect(request._source).toEqual({ @@ -407,6 +426,7 @@ describe('SearchSource', () => { expect(request.fields).toEqual(['@timestamp']); expect(request.script_fields).toEqual({ hello: {} }); expect(request.stored_fields).toEqual(['@timestamp', 'bar']); + expect(request.runtime_mappings).toEqual({ runtime_field: runtimeFieldDef }); }); test('filters request when a specific list of fields is provided with fieldsFromSource or fields', async () => { diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 0f0688c9fc11f9..554e8385881f23 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -461,12 +461,13 @@ export class SearchSource { searchRequest.indexType = this.getIndexType(index); // get some special field types from the index pattern - const { docvalueFields, scriptFields, storedFields } = index + const { docvalueFields, scriptFields, storedFields, runtimeFields } = index ? index.getComputedFields() : { docvalueFields: [], scriptFields: {}, storedFields: ['*'], + runtimeFields: {}, }; const fieldListProvided = !!body.fields; @@ -481,6 +482,7 @@ export class SearchSource { ...scriptFields, }; body.stored_fields = storedFields; + body.runtime_mappings = runtimeFields || {}; // apply source filters from index pattern if specified by the user let filteredDocvalueFields = docvalueFields; @@ -518,13 +520,18 @@ export class SearchSource { body.script_fields, Object.keys(body.script_fields).filter((f) => uniqFieldNames.includes(f)) ); + body.runtime_mappings = pick( + body.runtime_mappings, + Object.keys(body.runtime_mappings).filter((f) => uniqFieldNames.includes(f)) + ); } // request the remaining fields from stored_fields just in case, since the // fields API does not handle stored fields - const remainingFields = difference(uniqFieldNames, Object.keys(body.script_fields)).filter( - Boolean - ); + const remainingFields = difference(uniqFieldNames, [ + ...Object.keys(body.script_fields), + ...Object.keys(body.runtime_mappings), + ]).filter(Boolean); // only include unique values body.stored_fields = [...new Set(remainingFields)]; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d6bd896a584a46..28997de4517e79 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1265,6 +1265,7 @@ export type IMetricAggType = MetricAggType; export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); + addRuntimeField(name: string, runtimeField: RuntimeField): void; addScriptedField(name: string, script: string, fieldType?: string): Promise; readonly allowNoIndex: boolean; // (undocumented) @@ -1304,6 +1305,7 @@ export class IndexPattern implements IIndexPattern { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }; // (undocumented) getComputedFields(): { @@ -1313,6 +1315,7 @@ export class IndexPattern implements IIndexPattern { field: any; format: string; }[]; + runtimeFields: Record; }; // (undocumented) getFieldAttrs: () => { @@ -1352,6 +1355,7 @@ export class IndexPattern implements IIndexPattern { isTimeNanosBased(): boolean; // (undocumented) metaFields: string[]; + removeRuntimeField(name: string): void; removeScriptedField(fieldName: string): void; resetOriginalSavedObjectBody: () => void; // (undocumented) @@ -1402,6 +1406,8 @@ export interface IndexPatternAttributes { // (undocumented) intervalName?: string; // (undocumented) + runtimeFieldMap?: string; + // (undocumented) sourceFilters?: string; // (undocumented) timeFieldName?: string; @@ -1435,12 +1441,16 @@ export class IndexPatternField implements IFieldType { get esTypes(): string[] | undefined; // (undocumented) get filterable(): boolean; + get isMapped(): boolean | undefined; get lang(): string | undefined; set lang(lang: string | undefined); // (undocumented) get name(): string; // (undocumented) get readFromDocValues(): boolean; + // (undocumented) + get runtimeField(): RuntimeField | undefined; + set runtimeField(runtimeField: RuntimeField | undefined); get script(): string | undefined; set script(script: string | undefined); // (undocumented) @@ -1537,6 +1547,8 @@ export interface IndexPatternSpec { // @deprecated (undocumented) intervalName?: string; // (undocumented) + runtimeFieldMap?: Record; + // (undocumented) sourceFilters?: SourceFilter[]; // (undocumented) timeFieldName?: string; @@ -2580,8 +2592,9 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:22:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:20:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:63:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:133:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:139:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/search_source/search_source.ts:186:7 - (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:56:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index 3e3982dd58e57a..65f652df31d0c8 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -73,9 +73,10 @@ // sass-lint:disable-block no-important flex-grow: 0 !important; flex-basis: auto !important; - margin-right: -$euiSizeXS !important; &.kbnQueryBar__datePickerWrapper-isHidden { + // sass-lint:disable-block no-important + margin-right: -$euiSizeXS !important; width: 0; overflow: hidden; max-width: 0; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index ef8015ecaca26d..6a96fd8209a8d6 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -705,6 +705,7 @@ export type IMetricAggType = MetricAggType; export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); + addRuntimeField(name: string, runtimeField: RuntimeField): void; addScriptedField(name: string, script: string, fieldType?: string): Promise; readonly allowNoIndex: boolean; // (undocumented) @@ -746,6 +747,7 @@ export class IndexPattern implements IIndexPattern { type: string | undefined; typeMeta: string | undefined; allowNoIndex: true | undefined; + runtimeFieldMap: string | undefined; }; // (undocumented) getComputedFields(): { @@ -755,6 +757,7 @@ export class IndexPattern implements IIndexPattern { field: any; format: string; }[]; + runtimeFields: Record; }; // (undocumented) getFieldAttrs: () => { @@ -796,6 +799,7 @@ export class IndexPattern implements IIndexPattern { isTimeNanosBased(): boolean; // (undocumented) metaFields: string[]; + removeRuntimeField(name: string): void; removeScriptedField(fieldName: string): void; resetOriginalSavedObjectBody: () => void; // (undocumented) @@ -838,6 +842,8 @@ export interface IndexPatternAttributes { // (undocumented) intervalName?: string; // (undocumented) + runtimeFieldMap?: string; + // (undocumented) sourceFilters?: string; // (undocumented) timeFieldName?: string; @@ -1394,9 +1400,10 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:42:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:50:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:63:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:133:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:52:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:29:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:29:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:46:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx index ff10feea46d47c..b12de3f4496c5c 100644 --- a/src/plugins/discover/public/application/angular/directives/histogram.tsx +++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx @@ -154,6 +154,10 @@ export class DiscoverHistogram extends Component xAxisFormatter.convert(value)} /> { // show all the Rows $scope.minimumVisibleRows = $scope.hits; // delay scrolling to after the rows have been rendered - const bottomMarker = $element.find('#discoverBottomMarker'); - $timeout(() => { - bottomMarker.focus(); - // The anchor tag is not technically empty (it's a hack to make Safari scroll) - // so the browser will show a highlight: remove the focus once scrolled - $timeout(() => { - bottomMarker.blur(); - }, 0); - }, 0); + const bottomMarker = document.getElementById('discoverBottomMarker'); + const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + while ($scope.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { + await wait(50); + } + bottomMarker.focus(); + await wait(50); + bottomMarker.blur(); }; $scope.newQuery = function () { diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover/public/application/angular/discover_state.test.ts index 45e5e252e8361b..809664de5f073c 100644 --- a/src/plugins/discover/public/application/angular/discover_state.test.ts +++ b/src/plugins/discover/public/application/angular/discover_state.test.ts @@ -101,8 +101,9 @@ describe('Test discover state with legacy migration', () => { describe('createSearchSessionRestorationDataProvider', () => { let mockSavedSearch: SavedSearch = ({} as unknown) as SavedSearch; + const mockDataPlugin = dataPluginMock.createStartContract(); const searchSessionInfoProvider = createSearchSessionRestorationDataProvider({ - data: dataPluginMock.createStartContract(), + data: mockDataPlugin, appStateContainer: getState({ history: createBrowserHistory(), }).appStateContainer, @@ -124,4 +125,30 @@ describe('createSearchSessionRestorationDataProvider', () => { expect(await searchSessionInfoProvider.getName()).toBe('Discover'); }); }); + + describe('session state', () => { + test('restoreState has sessionId and initialState has not', async () => { + const searchSessionId = 'id'; + (mockDataPlugin.search.session.getSessionId as jest.Mock).mockImplementation( + () => searchSessionId + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.searchSessionId).toBeUndefined(); + expect(restoreState.searchSessionId).toBe(searchSessionId); + }); + + test('restoreState has absoluteTimeRange', async () => { + const relativeTime = 'relativeTime'; + const absoluteTime = 'absoluteTime'; + (mockDataPlugin.query.timefilter.timefilter.getTime as jest.Mock).mockImplementation( + () => relativeTime + ); + (mockDataPlugin.query.timefilter.timefilter.getAbsoluteTime as jest.Mock).mockImplementation( + () => absoluteTime + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.timeRange).toBe(relativeTime); + expect(restoreState.timeRange).toBe(absoluteTime); + }); + }); }); diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index fe05fceb858e5a..c769e263655abd 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -275,12 +275,12 @@ export function createSearchSessionRestorationDataProvider(deps: { initialState: createUrlGeneratorState({ ...deps, getSavedSearchId, - forceAbsoluteTime: false, + shouldRestoreSearchSession: false, }), restoreState: createUrlGeneratorState({ ...deps, getSavedSearchId, - forceAbsoluteTime: true, + shouldRestoreSearchSession: true, }), }; }, @@ -291,15 +291,12 @@ function createUrlGeneratorState({ appStateContainer, data, getSavedSearchId, - forceAbsoluteTime, + shouldRestoreSearchSession, }: { appStateContainer: StateContainer; data: DataPublicPluginStart; getSavedSearchId: () => string | undefined; - /** - * Can force time range from time filter to convert from relative to absolute time range - */ - forceAbsoluteTime: boolean; + shouldRestoreSearchSession: boolean; }): DiscoverUrlGeneratorState { const appState = appStateContainer.get(); return { @@ -307,10 +304,10 @@ function createUrlGeneratorState({ indexPatternId: appState.index, query: appState.query, savedSearchId: getSavedSearchId(), - timeRange: forceAbsoluteTime + timeRange: shouldRestoreSearchSession ? data.query.timefilter.timefilter.getAbsoluteTime() : data.query.timefilter.timefilter.getTime(), - searchSessionId: data.search.session.getSessionId(), + searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined, columns: appState.columns, sort: appState.sort, savedQuery: appState.savedQuery, diff --git a/src/plugins/discover/public/application/angular/helpers/point_series.ts b/src/plugins/discover/public/application/angular/helpers/point_series.ts index db58aebfd3ad7d..89317f77ef0606 100644 --- a/src/plugins/discover/public/application/angular/helpers/point_series.ts +++ b/src/plugins/discover/public/application/angular/helpers/point_series.ts @@ -62,6 +62,7 @@ export interface Chart { }>; xAxisOrderedValues: number[]; xAxisFormat: Dimension['format']; + yAxisFormat: Dimension['format']; xAxisLabel: Column['name']; yAxisLabel?: Column['name']; ordered: Ordered; @@ -76,7 +77,7 @@ export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => { chart.xAxisOrderedValues = uniq(table.rows.map((r) => r[xAccessor] as number)); chart.xAxisFormat = x.format; chart.xAxisLabel = table.columns[x.accessor].name; - + chart.yAxisFormat = y.format; const { intervalESUnit, intervalESValue, interval, bounds } = x.params; chart.ordered = { date: true, diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts index f72d65dd2ee566..1394ceab1dd18e 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts @@ -49,6 +49,7 @@ describe('getSharingData', () => { "should": Array [], }, }, + "runtime_mappings": Object {}, "script_fields": Object {}, "sort": Array [ Object { diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts index b7f7a5abb82b01..e2e1d7f05851ca 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts @@ -13,14 +13,24 @@ import { IKibanaResponse, KibanaResponseFactory } from 'kibana/server'; interface EsErrorHandlerParams { error: ApiError; response: KibanaResponseFactory; + handleCustomError?: () => IKibanaResponse; } /* * For errors returned by the new elasticsearch js client. */ -export const handleEsError = ({ error, response }: EsErrorHandlerParams): IKibanaResponse => { +export const handleEsError = ({ + error, + response, + handleCustomError, +}: EsErrorHandlerParams): IKibanaResponse => { // error.name is slightly better in terms of performance, since all errors now have name property if (error.name === 'ResponseError') { + // The consumer may sometimes want to provide a custom response + if (typeof handleCustomError === 'function') { + return handleCustomError(); + } + const { statusCode, body } = error as ResponseError; return response.customError({ statusCode, diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap b/src/plugins/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap index 9207c6467f6a92..151bd91750daa3 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap +++ b/src/plugins/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap @@ -170,6 +170,7 @@ exports[`CronEditor is rendered with a DAY frequency 1`] = `