diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1e2c657e29e5c..d7a05e1a79584 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -19,6 +19,11 @@ # Machine Learning /x-pack/plugins/ml/ @elastic/ml-ui +# Operations +/src/dev/ @elastic/kibana-operations +/src/setup_node_env/ @elastic/kibana-operations +/src/optimize/ @elastic/kibana-operations + # Platform /src/core/ @elastic/kibana-platform diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca9246619cea2..d26d73ea20561 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -356,7 +356,7 @@ Test runner arguments: `node scripts/jest -t 'stops both admin and data clients' src/core/server/elasticsearch/elasticsearch_service.test.ts` - Run the api integration test case whose description matches the given string: `node scripts/functional_tests_server --config test/api_integration/config.js` - `node scripts/functional_tests_runner --config test/api_integration/config.js --grep='should return 404 if id does not match any sample data sets'` + `node scripts/functional_test_runner --config test/api_integration/config.js --grep='should return 404 if id does not match any sample data sets'` ### Debugging Unit Tests diff --git a/docs/dev-tools/grokdebugger/getting-started.asciidoc b/docs/dev-tools/grokdebugger/getting-started.asciidoc deleted file mode 100644 index 88f2271110c92..0000000000000 --- a/docs/dev-tools/grokdebugger/getting-started.asciidoc +++ /dev/null @@ -1,124 +0,0 @@ -[role="xpack"] -[[grokdebugger-getting-started]] - -ifndef::gs-mini[] -=== Getting Started with the Grok Debugger -endif::gs-mini[] - -ifdef::gs-mini[] -== Getting Started with the Grok Debugger -endif::gs-mini[] - -++++ -Getting Started -++++ - -TIP: See the documentation about the ingest node -{ref}/grok-processor.html[grok processor] and the Logstash {logstash-ref}/plugins-filters-grok.html[grok filter] more info about grok. - -NOTE: If you're using {security}, you must have the `manage_pipeline` -permission in order to use the Grok Debugger. - -The Grok Debugger is automatically enabled in {kib}. It is located under the *DevTools* tab in {kib}. - -To start debugging grok patterns: - -. Open Kibana in your web browser and log in. If you are running Kibana -locally, go to `http://localhost:5601/`. - -. Click **DevTools** in the side navigation and then click **Grok Debugger** -on the top navigation bar. -+ -image::dev-tools/grokdebugger/images/grok-debugger.png["Grok Debugger UI"] - -. Under Sample Data, enter a sample message that is representative of the data you -want to parse. For example: -+ -[source,ruby] -------------------------------------------------------------------------------- -55.3.244.1 GET /index.html 15824 0.043 -------------------------------------------------------------------------------- - -. Under Grok Pattern, enter the grok pattern that you want to apply to the data. -+ -For example, to parse the log line in the example, you use: -+ -[source,ruby] -------------------------------------------------------------------------------- -%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:bytes} %{NUMBER:duration} -------------------------------------------------------------------------------- - -. Click the **Simulate** button. -+ -Under Structured Data, you'll see the simulated event that results from applying the grok -pattern: -+ -image::dev-tools/grokdebugger/images/grok-debugger-output.png["Viewing Grok Debugger Output"] -+ -Any errors in the pattern will appear at the top of the page. For example, -here you see a parse exception because the pattern name is misspelled as `WORDD` -and therefore can't be found in the pattern dictionary: -+ -image::dev-tools/grokdebugger/images/grok-debugger-error.png["Viewing Grok Debugger Errors"] -+ -You can click the **More** link to see more detail about the message. -+ -Click **OK** to dismiss the message and continue iterating over the grok pattern -until there are no errors and the output matches the event that you expect. - -//TODO: Update LS and ingest node docs with pointers to the new grok debugger. Replace references to the Heroku app. - -[float] -[[grokdebugger-custom-patterns]] -==== Testing Custom Patterns - -If the default grok pattern dictionary doesn't contain the patterns you need, -you may need to define custom patterns. You can use the Grok Debugger to test -and debug customer patterns. - -The custom patterns that you enter in the Grok Debugger are not saved. They're -only available for the current debugging session and have no side effects. - -To test a custom pattern: - -. Repeat the steps that you followed previously to enter the sample message and -grok pattern. For this example, let's use the following sample message: -+ -[source,ruby] -------------------------------------------------------------------------------- -Jan 1 06:25:43 mailserver14 postfix/cleanup[21403]: BEF25A72965: message-id=<20130101142543.5828399CCAF@mailserver14.example.com> -------------------------------------------------------------------------------- -+ -And this grok pattern: -+ -[source,ruby] -------------------------------------------------------------------------------- -%{SYSLOGBASE} %{POSTFIX_QUEUEID:queue_id}: %{MSG:syslog_message} -------------------------------------------------------------------------------- -+ -Notice that the grok pattern references custom patterns called `POSTFIX_QUEUEID` -and `MSG`. - -. Expand **Custom Patterns** and enter pattern definitions for any custom -patterns that you want to use in the grok expression. Each pattern definition -must be specified on its own line. -+ -For the grok pattern in the example, you need to specify pattern definitions -for `POSTFIX_QUEUEID` and `MSG`: -+ -[source,ruby] -------------------------------------------------------------------------------- -POSTFIX_QUEUEID [0-9A-F]{10,11} -MSG message-id=<%{GREEDYDATA}> -------------------------------------------------------------------------------- - -. Click the **Simulate** button. -+ -Under Output, you'll see the simulated output event that results from applying -the grok pattern that contains the custom pattern: -+ -image::dev-tools/grokdebugger/images/grok-debugger-custom-pattern.png["Debugging a custom pattern"] -+ -If an error occurs, you can view the error message and continue iterating over -the custom pattern until there are no errors and the output matches the event -that you expect. diff --git a/docs/dev-tools/grokdebugger/images/grok-debugger-custom-pattern.png b/docs/dev-tools/grokdebugger/images/grok-debugger-custom-pattern.png index f0149c7b0f36c..68b3669e4916a 100644 Binary files a/docs/dev-tools/grokdebugger/images/grok-debugger-custom-pattern.png and b/docs/dev-tools/grokdebugger/images/grok-debugger-custom-pattern.png differ diff --git a/docs/dev-tools/grokdebugger/images/grok-debugger-error.png b/docs/dev-tools/grokdebugger/images/grok-debugger-error.png deleted file mode 100644 index 3ce4cb618f5ae..0000000000000 Binary files a/docs/dev-tools/grokdebugger/images/grok-debugger-error.png and /dev/null differ diff --git a/docs/dev-tools/grokdebugger/images/grok-debugger-output.png b/docs/dev-tools/grokdebugger/images/grok-debugger-output.png deleted file mode 100644 index 8ea217d3653a7..0000000000000 Binary files a/docs/dev-tools/grokdebugger/images/grok-debugger-output.png and /dev/null differ diff --git a/docs/dev-tools/grokdebugger/images/grok-debugger-overview.png b/docs/dev-tools/grokdebugger/images/grok-debugger-overview.png index 3cda516996708..3c5ce010129d5 100644 Binary files a/docs/dev-tools/grokdebugger/images/grok-debugger-overview.png and b/docs/dev-tools/grokdebugger/images/grok-debugger-overview.png differ diff --git a/docs/dev-tools/grokdebugger/images/grok-debugger-pattern.png b/docs/dev-tools/grokdebugger/images/grok-debugger-pattern.png deleted file mode 100644 index 927c166e34477..0000000000000 Binary files a/docs/dev-tools/grokdebugger/images/grok-debugger-pattern.png and /dev/null differ diff --git a/docs/dev-tools/grokdebugger/images/grok-debugger.png b/docs/dev-tools/grokdebugger/images/grok-debugger.png deleted file mode 100644 index cb25a6be18138..0000000000000 Binary files a/docs/dev-tools/grokdebugger/images/grok-debugger.png and /dev/null differ diff --git a/docs/dev-tools/grokdebugger/index.asciidoc b/docs/dev-tools/grokdebugger/index.asciidoc index ca4ed0dd05952..1fb9f945d8636 100644 --- a/docs/dev-tools/grokdebugger/index.asciidoc +++ b/docs/dev-tools/grokdebugger/index.asciidoc @@ -1,25 +1,119 @@ [role="xpack"] [[xpack-grokdebugger]] -== Debugging Grok Expressions +== Debugging grok expressions -Grok is a pattern matching syntax that you can use to parse arbitrary text and -structure it. Grok is perfect for parsing syslog logs, apache and other -webserver logs, mysql logs, and in general, any log format that is generally -written for humans and not computer consumption. +You can build and debug grok patterns in the {kib} *Grok Debugger* +before you use them in your data processing pipelines. Grok is a pattern +matching syntax that you can use to parse arbitrary text and +structure it. Grok is good for parsing syslog, apache, and other +webserver logs, mysql logs, and in general, any log format that is +written for human consumption. Grok patterns are supported in the ingest node {ref}/grok-processor.html[grok processor] and the Logstash -{logstash-ref}/plugins-filters-grok.html[grok filter]. The Elastic Stack ships -with over 120 reusable grok patterns. See -https://github.com/elastic/elasticsearch/tree/master/modules/ingest-common/src/main/resources/patterns[Ingest node grok patterns] and https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns[Logstash grok patterns] -for the full list of patterns. +{logstash-ref}/plugins-filters-grok.html[grok filter]. See +{logstash-ref}/plugins-filters-grok.html#_grok_basics[grok basics] +for more information on the syntax for a grok pattern. -You can build and debug grok patterns in the Grok Debugger tool in {kib} -before you use them in your data processing pipelines. Because +The Elastic Stack ships +with more than 120 reusable grok patterns. See +https://github.com/elastic/elasticsearch/tree/master/libs/grok/src/main/resources/patterns[Ingest node grok patterns] and https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns[Logstash grok patterns] +for the complete list of patterns. + +Because ingest node and Logstash share the same grok implementation and pattern -libraries, any grok pattern that you create in the Grok Debugger will work +libraries, any grok pattern that you create in the *Grok Debugger* will work in ingest node and Logstash. +[float] +[[grokdebugger-getting-started]] +=== Getting started with the Grok Debugger + +This example walks you through using the *Grok Debugger*. This tool +is automatically enabled in {kib}. + +NOTE: If you're using {security}, you must have the `manage_pipeline` +permission to use the Grok Debugger. + +. In the side navigation, click *DevTools*, then open the *Grok Debugger*. +. In *Sample Data*, enter a message that is representative of the data that you +want to parse. For example: ++ +[source,ruby] +------------------------------------------------------------------------------- +55.3.244.1 GET /index.html 15824 0.043 +------------------------------------------------------------------------------- + +. In *Grok Pattern*, enter the grok pattern that you want to apply to the data. ++ +To parse the log line in this example, use: ++ +[source,ruby] +------------------------------------------------------------------------------- +%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:bytes} %{NUMBER:duration} +------------------------------------------------------------------------------- + +. Click **Simulate**. ++ +You'll see the simulated event that results from applying the grok +pattern. ++ +[role="screenshot"] image::dev-tools/grokdebugger/images/grok-debugger-overview.png["Grok Debugger"] -include::getting-started.asciidoc[] + +//TODO: Update LS and ingest node docs with pointers to the new grok debugger. Replace references to the Heroku app. + +[float] +[[grokdebugger-custom-patterns]] +=== Testing custom patterns + +If the default grok pattern dictionary doesn't contain the patterns you need, +you can define, test, and debug custom patterns using the Grok Debugger. + +Custom patterns that you enter in the Grok Debugger are not saved. Custom patterns +are only available for the current debugging session and have no side effects. + +Follow this example to define a custom pattern. + +. In *Sample Data*, enter the following sample message: ++ +[source,ruby] +------------------------------------------------------------------------------- +Jan 1 06:25:43 mailserver14 postfix/cleanup[21403]: BEF25A72965: message-id=<20130101142543.5828399CCAF@mailserver14.example.com> +------------------------------------------------------------------------------- + +. Enter this grok pattern: ++ +[source,ruby] +------------------------------------------------------------------------------- +%{SYSLOGBASE} %{POSTFIX_QUEUEID:queue_id}: %{MSG:syslog_message} +------------------------------------------------------------------------------- ++ +Notice that the grok pattern references custom patterns called `POSTFIX_QUEUEID` +and `MSG`. + +. Expand **Custom Patterns** and enter pattern definitions for the custom +patterns that you want to use in the grok expression. You must specify each pattern definition +on its own line. ++ +For this example, you must specify pattern definitions +for `POSTFIX_QUEUEID` and `MSG`: ++ +[source,ruby] +------------------------------------------------------------------------------- +POSTFIX_QUEUEID [0-9A-F]{10,11} +MSG message-id=<%{GREEDYDATA}> +------------------------------------------------------------------------------- + +. Click **Simulate**. ++ +You'll see the simulated output event that results from applying +the grok pattern that contains the custom pattern: ++ +[role="screenshot"] +image::dev-tools/grokdebugger/images/grok-debugger-custom-pattern.png["Debugging a custom pattern"] ++ +If an error occurs, you can continue iterating over +the custom pattern until the output matches the event +that you expect. diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md index 344a32e8596f1..67af8d3fdb1a2 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.md @@ -4,7 +4,7 @@ ## CoreSetup interface -Core services exposed to the start lifecycle +Core services exposed to the setup lifecycle Signature: diff --git a/docs/development/core/public/kibana-plugin-public.corestart.http.md b/docs/development/core/public/kibana-plugin-public.corestart.http.md new file mode 100644 index 0000000000000..e70b60b8d5285 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.corestart.http.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [http](./kibana-plugin-public.corestart.http.md) + +## CoreStart.http property + +[HttpStart](./kibana-plugin-public.httpstart.md) + +Signature: + +```typescript +http: HttpStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index 8009738087418..b9124a185976c 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -4,6 +4,8 @@ ## CoreStart interface +Core services exposed to the start lifecycle + Signature: ```typescript @@ -16,6 +18,7 @@ export interface CoreStart | --- | --- | --- | | [application](./kibana-plugin-public.corestart.application.md) | ApplicationStart | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | [basePath](./kibana-plugin-public.corestart.basepath.md) | BasePathStart | [BasePathStart](./kibana-plugin-public.basepathstart.md) | +| [http](./kibana-plugin-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-public.httpstart.md) | | [i18n](./kibana-plugin-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-public.i18nstart.md) | | [injectedMetadata](./kibana-plugin-public.corestart.injectedmetadata.md) | InjectedMetadataStart | [InjectedMetadataStart](./kibana-plugin-public.injectedmetadatastart.md) | | [notifications](./kibana-plugin-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.md b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.md new file mode 100644 index 0000000000000..ff326a2122b3c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.md @@ -0,0 +1,21 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) + +## FatalErrorInfo interface + +Represents the `message` and `stack` of a fatal Error + +Signature: + +```typescript +export interface FatalErrorInfo +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [message](./kibana-plugin-public.fatalerrorinfo.message.md) | string | | +| [stack](./kibana-plugin-public.fatalerrorinfo.stack.md) | string | undefined | | + diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.message.md b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.message.md new file mode 100644 index 0000000000000..68b2c912a9030 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.message.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) > [message](./kibana-plugin-public.fatalerrorinfo.message.md) + +## FatalErrorInfo.message property + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.stack.md b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.stack.md new file mode 100644 index 0000000000000..c251a4866cbda --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorinfo.stack.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) > [stack](./kibana-plugin-public.fatalerrorinfo.stack.md) + +## FatalErrorInfo.stack property + +Signature: + +```typescript +stack: string | undefined; +``` diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.get$.md b/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.get$.md index d962663d317b1..e3bc308a3dd8b 100644 --- a/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.get$.md +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.get$.md @@ -9,5 +9,5 @@ An Observable that will emit whenever a fatal error is added with `add()` Signature: ```typescript -get$: () => Rx.Observable; +get$: () => Rx.Observable; ``` diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.md b/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.md index 889d71fc81e76..c2e79ca191859 100644 --- a/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.md +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorssetup.md @@ -17,5 +17,5 @@ export interface FatalErrorsSetup | Property | Type | Description | | --- | --- | --- | | [add](./kibana-plugin-public.fatalerrorssetup.add.md) | (error: string | Error, source?: string) => never | Add a new fatal error. This will stop the Kibana Public Core and display a fatal error screen with details about the Kibana build and the error. | -| [get$](./kibana-plugin-public.fatalerrorssetup.get$.md) | () => Rx.Observable<ErrorInfo> | An Observable that will emit whenever a fatal error is added with add() | +| [get$](./kibana-plugin-public.fatalerrorssetup.get$.md) | () => Rx.Observable<FatalErrorInfo> | An Observable that will emit whenever a fatal error is added with add() | diff --git a/docs/development/core/public/kibana-plugin-public.flyoutref.close.md b/docs/development/core/public/kibana-plugin-public.flyoutref.close.md deleted file mode 100644 index 5e3271f26c162..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.flyoutref.close.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FlyoutRef](./kibana-plugin-public.flyoutref.md) > [close](./kibana-plugin-public.flyoutref.close.md) - -## FlyoutRef.close() method - -Closes the referenced flyout if it's still open which in turn will resolve the `onClose` Promise. If the flyout had already been closed this method does nothing. - -Signature: - -```typescript -close(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/core/public/kibana-plugin-public.flyoutref.md b/docs/development/core/public/kibana-plugin-public.flyoutref.md deleted file mode 100644 index fc5932955dc76..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.flyoutref.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FlyoutRef](./kibana-plugin-public.flyoutref.md) - -## FlyoutRef class - -A FlyoutRef is a reference to an opened flyout panel. It offers methods to close the flyout panel again. If you open a flyout panel you should make sure you call `close()` when it should be closed. Since a flyout could also be closed by a user or from another flyout being opened, you must bind to the `onClose` Promise on the FlyoutRef instance. The Promise will resolve whenever the flyout was closed at which point you should discard the FlyoutRef. - -Signature: - -```typescript -export declare class FlyoutRef -``` - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [onClose](./kibana-plugin-public.flyoutref.onclose.md) | | Promise<void> | An Promise that will resolve once this flyout is closed.Flyouts can close from user interaction, calling close() on the flyout reference or another call to openFlyout() replacing your flyout. | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [close()](./kibana-plugin-public.flyoutref.close.md) | | Closes the referenced flyout if it's still open which in turn will resolve the onClose Promise. If the flyout had already been closed this method does nothing. | - diff --git a/docs/development/core/public/kibana-plugin-public.flyoutref.onclose.md b/docs/development/core/public/kibana-plugin-public.flyoutref.onclose.md deleted file mode 100644 index 2e58f7563d291..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.flyoutref.onclose.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FlyoutRef](./kibana-plugin-public.flyoutref.md) > [onClose](./kibana-plugin-public.flyoutref.onclose.md) - -## FlyoutRef.onClose property - -An Promise that will resolve once this flyout is closed. - -Flyouts can close from user interaction, calling `close()` on the flyout reference or another call to `openFlyout()` replacing your flyout. - -Signature: - -```typescript -readonly onClose: Promise; -``` diff --git a/docs/development/core/public/kibana-plugin-public.httpstart.md b/docs/development/core/public/kibana-plugin-public.httpstart.md new file mode 100644 index 0000000000000..333c6c140ea50 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpstart.md @@ -0,0 +1,12 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpStart](./kibana-plugin-public.httpstart.md) + +## HttpStart type + + +Signature: + +```typescript +export declare type HttpStart = ReturnType; +``` diff --git a/docs/development/core/public/kibana-plugin-public.i18nstart.md b/docs/development/core/public/kibana-plugin-public.i18nstart.md index 628ded0c0ef0d..297e4f9cee5c7 100644 --- a/docs/development/core/public/kibana-plugin-public.i18nstart.md +++ b/docs/development/core/public/kibana-plugin-public.i18nstart.md @@ -4,6 +4,7 @@ ## I18nStart type + Signature: ```typescript diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getkibanabuildnumber.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getkibanabuildnumber.md new file mode 100644 index 0000000000000..e5a88b65b5620 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.getkibanabuildnumber.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) > [getKibanaBuildNumber](./kibana-plugin-public.injectedmetadatasetup.getkibanabuildnumber.md) + +## InjectedMetadataSetup.getKibanaBuildNumber property + +Signature: + +```typescript +getKibanaBuildNumber: () => number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.md b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.md index 4265c55162289..f6f621cdada45 100644 --- a/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.md +++ b/docs/development/core/public/kibana-plugin-public.injectedmetadatasetup.md @@ -20,6 +20,7 @@ export interface InjectedMetadataSetup | [getCspConfig](./kibana-plugin-public.injectedmetadatasetup.getcspconfig.md) | () => {`

` warnLegacyBrowsers: boolean;`

` } | | | [getInjectedVar](./kibana-plugin-public.injectedmetadatasetup.getinjectedvar.md) | (name: string, defaultValue?: any) => unknown | | | [getInjectedVars](./kibana-plugin-public.injectedmetadatasetup.getinjectedvars.md) | () => {`

` [key: string]: unknown;`

` } | | +| [getKibanaBuildNumber](./kibana-plugin-public.injectedmetadatasetup.getkibanabuildnumber.md) | () => number | | | [getKibanaVersion](./kibana-plugin-public.injectedmetadatasetup.getkibanaversion.md) | () => string | | | [getLegacyMetadata](./kibana-plugin-public.injectedmetadatasetup.getlegacymetadata.md) | () => {`

` app: unknown;`

` translations: unknown;`

` bundleId: string;`

` nav: LegacyNavLink[];`

` version: string;`

` branch: string;`

` buildNum: number;`

` buildSha: string;`

` basePath: string;`

` serverName: string;`

` devMode: boolean;`

` uiSettings: {`

` defaults: UiSettingsState;`

` user?: UiSettingsState | undefined;`

` };`

` } | | | [getPlugins](./kibana-plugin-public.injectedmetadatasetup.getplugins.md) | () => Array<{`

` id: string;`

` plugin: DiscoveredPlugin;`

` }> | An array of frontend plugins in topological order. | diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 27df06fbb5630..0e496c982fd70 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -8,7 +8,6 @@ | Class | Description | | --- | --- | -| [FlyoutRef](./kibana-plugin-public.flyoutref.md) | A FlyoutRef is a reference to an opened flyout panel. It offers methods to close the flyout panel again. If you open a flyout panel you should make sure you call close() when it should be closed. Since a flyout could also be closed by a user or from another flyout being opened, you must bind to the onClose Promise on the FlyoutRef instance. The Promise will resolve whenever the flyout was closed at which point you should discard the FlyoutRef. | | [ToastsApi](./kibana-plugin-public.toastsapi.md) | | | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | | @@ -23,17 +22,20 @@ | [ChromeBadge](./kibana-plugin-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | | [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | | -| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the start lifecycle | -| [CoreStart](./kibana-plugin-public.corestart.md) | | +| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the setup lifecycle | +| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the start lifecycle | +| [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [I18nSetup](./kibana-plugin-public.i18nsetup.md) | I18nSetup.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | | [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) | Provides access to the metadata injected by the server into the page | | [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | | | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | | +| [OverlayRef](./kibana-plugin-public.overlayref.md) | | | [OverlayStart](./kibana-plugin-public.overlaystart.md) | | | [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer | | [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) | The available core services passed to a plugin's Plugin#setup method. | +| [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) | The available core services passed to a plugin's Plugin#start method. | | [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | | ## Type Aliases @@ -44,6 +46,7 @@ | [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | | [ChromeSetup](./kibana-plugin-public.chromesetup.md) | | | [HttpSetup](./kibana-plugin-public.httpsetup.md) | | +| [HttpStart](./kibana-plugin-public.httpstart.md) | | | [I18nStart](./kibana-plugin-public.i18nstart.md) | | | [InjectedMetadataStart](./kibana-plugin-public.injectedmetadatastart.md) | | | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | diff --git a/docs/development/core/public/kibana-plugin-public.overlayref.close.md b/docs/development/core/public/kibana-plugin-public.overlayref.close.md new file mode 100644 index 0000000000000..e6e4bf2f7035b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.overlayref.close.md @@ -0,0 +1,17 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayRef](./kibana-plugin-public.overlayref.md) > [close](./kibana-plugin-public.overlayref.close.md) + +## OverlayRef.close() method + +Closes the referenced overlay if it's still open which in turn will resolve the `onClose` Promise. If the overlay had already been closed this method does nothing. + +Signature: + +```typescript +close(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/core/public/kibana-plugin-public.overlayref.md b/docs/development/core/public/kibana-plugin-public.overlayref.md new file mode 100644 index 0000000000000..543a2c52b3619 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.overlayref.md @@ -0,0 +1,24 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayRef](./kibana-plugin-public.overlayref.md) + +## OverlayRef interface + +Signature: + +```typescript +export interface OverlayRef +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [onClose](./kibana-plugin-public.overlayref.onclose.md) | Promise<void> | A Promise that will resolve once this overlay is closed.Overlays can close from user interaction, calling close() on the overlay reference or another overlay replacing yours via openModal or openFlyout. | + +## Methods + +| Method | Description | +| --- | --- | +| [close()](./kibana-plugin-public.overlayref.close.md) | Closes the referenced overlay if it's still open which in turn will resolve the onClose Promise. If the overlay had already been closed this method does nothing. | + diff --git a/docs/development/core/public/kibana-plugin-public.overlayref.onclose.md b/docs/development/core/public/kibana-plugin-public.overlayref.onclose.md new file mode 100644 index 0000000000000..d8f0351df8622 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.overlayref.onclose.md @@ -0,0 +1,15 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayRef](./kibana-plugin-public.overlayref.md) > [onClose](./kibana-plugin-public.overlayref.onclose.md) + +## OverlayRef.onClose property + +A Promise that will resolve once this overlay is closed. + +Overlays can close from user interaction, calling `close()` on the overlay reference or another overlay replacing yours via `openModal` or `openFlyout`. + +Signature: + +```typescript +onClose: Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-public.overlaystart.md b/docs/development/core/public/kibana-plugin-public.overlaystart.md index 5829e6c2ce34d..639a2b461d56a 100644 --- a/docs/development/core/public/kibana-plugin-public.overlaystart.md +++ b/docs/development/core/public/kibana-plugin-public.overlaystart.md @@ -15,5 +15,6 @@ export interface OverlayStart | Property | Type | Description | | --- | --- | --- | -| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | (flyoutChildren: React.ReactNode, flyoutProps?: {`

` closeButtonAriaLabel?: string;`

` 'data-test-subj'?: string;`

` }) => FlyoutRef | | +| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | (flyoutChildren: React.ReactNode, flyoutProps?: {`

` closeButtonAriaLabel?: string;`

` 'data-test-subj'?: string;`

` }) => OverlayRef | | +| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | (modalChildren: React.ReactNode, modalProps?: {`

` closeButtonAriaLabel?: string;`

` 'data-test-subj'?: string;`

` }) => OverlayRef | | diff --git a/docs/development/core/public/kibana-plugin-public.overlaystart.openflyout.md b/docs/development/core/public/kibana-plugin-public.overlaystart.openflyout.md index f030c842c98f8..33d6ddf1a58e5 100644 --- a/docs/development/core/public/kibana-plugin-public.overlaystart.openflyout.md +++ b/docs/development/core/public/kibana-plugin-public.overlaystart.openflyout.md @@ -10,5 +10,5 @@ openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: { closeButtonAriaLabel?: string; 'data-test-subj'?: string; - }) => FlyoutRef; + }) => OverlayRef; ``` diff --git a/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md b/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md new file mode 100644 index 0000000000000..7d26a7ad6a181 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md @@ -0,0 +1,14 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayStart](./kibana-plugin-public.overlaystart.md) > [openModal](./kibana-plugin-public.overlaystart.openmodal.md) + +## OverlayStart.openModal property + +Signature: + +```typescript +openModal: (modalChildren: React.ReactNode, modalProps?: { + closeButtonAriaLabel?: string; + 'data-test-subj'?: string; + }) => OverlayRef; +``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.application.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.application.md new file mode 100644 index 0000000000000..9c718893005db --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.application.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [application](./kibana-plugin-public.pluginstartcontext.application.md) + +## PluginStartContext.application property + +Signature: + +```typescript +application: Pick; +``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.basepath.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.basepath.md new file mode 100644 index 0000000000000..a4c44fedca5fe --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.basepath.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [basePath](./kibana-plugin-public.pluginstartcontext.basepath.md) + +## PluginStartContext.basePath property + +Signature: + +```typescript +basePath: BasePathStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.http.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.http.md new file mode 100644 index 0000000000000..a70a100cc7e4c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.http.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [http](./kibana-plugin-public.pluginstartcontext.http.md) + +## PluginStartContext.http property + +Signature: + +```typescript +http: HttpStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.i18n.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.i18n.md new file mode 100644 index 0000000000000..9e1896aa9fc88 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.i18n.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [i18n](./kibana-plugin-public.pluginstartcontext.i18n.md) + +## PluginStartContext.i18n property + +Signature: + +```typescript +i18n: I18nStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.md new file mode 100644 index 0000000000000..6b0fa1be7ea83 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) + +## PluginStartContext interface + +The available core services passed to a plugin's `Plugin#start` method. + +Signature: + +```typescript +export interface PluginStartContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [application](./kibana-plugin-public.pluginstartcontext.application.md) | Pick<ApplicationStart, 'capabilities'> | | +| [basePath](./kibana-plugin-public.pluginstartcontext.basepath.md) | BasePathStart | | +| [http](./kibana-plugin-public.pluginstartcontext.http.md) | HttpStart | | +| [i18n](./kibana-plugin-public.pluginstartcontext.i18n.md) | I18nStart | | +| [notifications](./kibana-plugin-public.pluginstartcontext.notifications.md) | NotificationsStart | | +| [overlays](./kibana-plugin-public.pluginstartcontext.overlays.md) | OverlayStart | | + diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.notifications.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.notifications.md new file mode 100644 index 0000000000000..e9972f5e7ff4c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.notifications.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [notifications](./kibana-plugin-public.pluginstartcontext.notifications.md) + +## PluginStartContext.notifications property + +Signature: + +```typescript +notifications: NotificationsStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginstartcontext.overlays.md b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.overlays.md new file mode 100644 index 0000000000000..c299389d3c922 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.pluginstartcontext.overlays.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginStartContext](./kibana-plugin-public.pluginstartcontext.md) > [overlays](./kibana-plugin-public.pluginstartcontext.overlays.md) + +## PluginStartContext.overlays property + +Signature: + +```typescript +overlays: OverlayStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.md index 30fdfa73dd215..985b42cd0063b 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.md @@ -11,12 +11,6 @@ export declare class UiSettingsClient ``` -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [params](./kibana-plugin-public.uisettingsclient.params.md) | | UiSettingsClientParams | | - ## Methods | Method | Modifiers | Description | diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.params.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.params.md deleted file mode 100644 index 71b3cfaae87fb..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.params.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [params](./kibana-plugin-public.uisettingsclient.params.md) - -## UiSettingsClient.params property - -Signature: - -```typescript -readonly params: UiSettingsClientParams; -``` diff --git a/docs/development/core/server/kibana-plugin-server.configservice.atpath.md b/docs/development/core/server/kibana-plugin-server.configservice.atpath.md deleted file mode 100644 index 5ae66deb74856..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.atpath.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [atPath](./kibana-plugin-server.configservice.atpath.md) - -## ConfigService.atPath() method - -Reads the subset of the config at the specified `path` and validates it against the static `schema` on the given `ConfigClass`. - -Signature: - -```typescript -atPath, TConfig>(path: ConfigPath, ConfigClass: ConfigWithSchema): Observable; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| path | ConfigPath | The path to the desired subset of the config. | -| ConfigClass | ConfigWithSchema<TSchema, TConfig> | A class (not an instance of a class) that contains a static schema that we validate the config at the given path against. | - -Returns: - -`Observable` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.getconfig$.md b/docs/development/core/server/kibana-plugin-server.configservice.getconfig$.md deleted file mode 100644 index 6a9e288a8160e..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.getconfig$.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [getConfig$](./kibana-plugin-server.configservice.getconfig$.md) - -## ConfigService.getConfig$() method - -Returns the full config object observable. This is not intended for "normal use", but for features that \_need\_ access to the full object. - -Signature: - -```typescript -getConfig$(): Observable; -``` -Returns: - -`Observable` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.getunusedpaths.md b/docs/development/core/server/kibana-plugin-server.configservice.getunusedpaths.md deleted file mode 100644 index 9026672abb78e..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.getunusedpaths.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [getUnusedPaths](./kibana-plugin-server.configservice.getunusedpaths.md) - -## ConfigService.getUnusedPaths() method - -Signature: - -```typescript -getUnusedPaths(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.getusedpaths.md b/docs/development/core/server/kibana-plugin-server.configservice.getusedpaths.md deleted file mode 100644 index a29b075a8b3e4..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.getusedpaths.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [getUsedPaths](./kibana-plugin-server.configservice.getusedpaths.md) - -## ConfigService.getUsedPaths() method - -Signature: - -```typescript -getUsedPaths(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.isenabledatpath.md b/docs/development/core/server/kibana-plugin-server.configservice.isenabledatpath.md deleted file mode 100644 index 08c9985145f35..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.isenabledatpath.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [isEnabledAtPath](./kibana-plugin-server.configservice.isenabledatpath.md) - -## ConfigService.isEnabledAtPath() method - -Signature: - -```typescript -isEnabledAtPath(path: ConfigPath): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| path | ConfigPath | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.md b/docs/development/core/server/kibana-plugin-server.configservice.md deleted file mode 100644 index 34a2bd9cecaab..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) - -## ConfigService class - - -Signature: - -```typescript -export declare class ConfigService -``` - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [atPath(path, ConfigClass)](./kibana-plugin-server.configservice.atpath.md) | | Reads the subset of the config at the specified path and validates it against the static schema on the given ConfigClass. | -| [getConfig$()](./kibana-plugin-server.configservice.getconfig$.md) | | Returns the full config object observable. This is not intended for "normal use", but for features that \_need\_ access to the full object. | -| [getUnusedPaths()](./kibana-plugin-server.configservice.getunusedpaths.md) | | | -| [getUsedPaths()](./kibana-plugin-server.configservice.getusedpaths.md) | | | -| [isEnabledAtPath(path)](./kibana-plugin-server.configservice.isenabledatpath.md) | | | -| [optionalAtPath(path, ConfigClass)](./kibana-plugin-server.configservice.optionalatpath.md) | | Same as atPath, but returns undefined if there is no config at the specified path.[ConfigService.atPath()](./kibana-plugin-server.configservice.atpath.md) | - diff --git a/docs/development/core/server/kibana-plugin-server.configservice.optionalatpath.md b/docs/development/core/server/kibana-plugin-server.configservice.optionalatpath.md deleted file mode 100644 index 8efc64568f830..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.configservice.optionalatpath.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigService](./kibana-plugin-server.configservice.md) > [optionalAtPath](./kibana-plugin-server.configservice.optionalatpath.md) - -## ConfigService.optionalAtPath() method - -Same as `atPath`, but returns `undefined` if there is no config at the specified path. - -[ConfigService.atPath()](./kibana-plugin-server.configservice.atpath.md) - -Signature: - -```typescript -optionalAtPath, TConfig>(path: ConfigPath, ConfigClass: ConfigWithSchema): Observable; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| path | ConfigPath | | -| ConfigClass | ConfigWithSchema<TSchema, TConfig> | | - -Returns: - -`Observable` - diff --git a/docs/development/core/server/kibana-plugin-server.corestart.md b/docs/development/core/server/kibana-plugin-server.corestart.md index 119036da61ee1..90cc4549eb3bf 100644 --- a/docs/development/core/server/kibana-plugin-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-server.corestart.md @@ -4,6 +4,7 @@ ## CoreStart interface + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md new file mode 100644 index 0000000000000..a870cd7d7a61e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) > [configPath](./kibana-plugin-server.discoveredplugin.configpath.md) + +## DiscoveredPlugin.configPath property + +Root configuration path used by the plugin, defaults to "id". + +Signature: + +```typescript +readonly configPath: ConfigPath; +``` diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.id.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.id.md new file mode 100644 index 0000000000000..6f75b399ace83 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.id.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) > [id](./kibana-plugin-server.discoveredplugin.id.md) + +## DiscoveredPlugin.id property + +Identifier of the plugin. + +Signature: + +```typescript +readonly id: PluginName; +``` diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.md new file mode 100644 index 0000000000000..d03d8a8f4a17a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.md @@ -0,0 +1,23 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) + +## DiscoveredPlugin interface + +Small container object used to expose information about discovered plugins that may or may not have been started. + +Signature: + +```typescript +export interface DiscoveredPlugin +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [configPath](./kibana-plugin-server.discoveredplugin.configpath.md) | ConfigPath | Root configuration path used by the plugin, defaults to "id". | +| [id](./kibana-plugin-server.discoveredplugin.id.md) | PluginName | Identifier of the plugin. | +| [optionalPlugins](./kibana-plugin-server.discoveredplugin.optionalplugins.md) | ReadonlyArray<PluginName> | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | +| [requiredPlugins](./kibana-plugin-server.discoveredplugin.requiredplugins.md) | ReadonlyArray<PluginName> | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | + diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.optionalplugins.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.optionalplugins.md new file mode 100644 index 0000000000000..1ebc425f44ce5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.optionalplugins.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) > [optionalPlugins](./kibana-plugin-server.discoveredplugin.optionalplugins.md) + +## DiscoveredPlugin.optionalPlugins property + +An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. + +Signature: + +```typescript +readonly optionalPlugins: ReadonlyArray; +``` diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.requiredplugins.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.requiredplugins.md new file mode 100644 index 0000000000000..4ca74797289f7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.requiredplugins.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) > [requiredPlugins](./kibana-plugin-server.discoveredplugin.requiredplugins.md) + +## DiscoveredPlugin.requiredPlugins property + +An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. + +Signature: + +```typescript +readonly requiredPlugins: ReadonlyArray; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index d423cfb5f4dea..f7f7707b5657e 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -8,7 +8,7 @@ Signature: ```typescript -export declare class KibanaRequest +export declare class KibanaRequest ``` ## Properties @@ -27,4 +27,5 @@ export declare class KibanaRequest | --- | --- | --- | | [from(req, routeSchemas)](./kibana-plugin-server.kibanarequest.from.md) | static | Factory for creating requests. Validates the request before creating an instance of a KibanaRequest. | | [getFilteredHeaders(headersToKeep)](./kibana-plugin-server.kibanarequest.getfilteredheaders.md) | | | +| [unstable\_getIncomingMessage()](./kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md) | | | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md new file mode 100644 index 0000000000000..d4f3c1b54a6cd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md @@ -0,0 +1,15 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [unstable\_getIncomingMessage](./kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md) + +## KibanaRequest.unstable\_getIncomingMessage() method + +Signature: + +```typescript +unstable_getIncomingMessage(): import("http").IncomingMessage; +``` +Returns: + +`import("http").IncomingMessage` + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index f3e11fca6b280..027aef60265a2 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -9,7 +9,6 @@ | Class | Description | | --- | --- | | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | -| [ConfigService](./kibana-plugin-server.configservice.md) | | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | | | [Router](./kibana-plugin-server.router.md) | | | [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API | @@ -22,6 +21,7 @@ | [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [CoreSetup](./kibana-plugin-server.coresetup.md) | | | [CoreStart](./kibana-plugin-server.corestart.md) | | +| [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | | [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | @@ -31,6 +31,8 @@ | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) | Context passed to the plugins setup method. | +| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | +| [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | | [PluginStartContext](./kibana-plugin-server.pluginstartcontext.md) | Context passed to the plugins start method. | ## Type Aliases diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md index cd88ef8fc5dda..e6a79a13dd436 100644 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md +++ b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md @@ -19,4 +19,5 @@ export interface OnRequestToolkit | [next](./kibana-plugin-server.onrequesttoolkit.next.md) | () => OnRequestResult | To pass request to the next handler | | [redirected](./kibana-plugin-server.onrequesttoolkit.redirected.md) | (url: string) => OnRequestResult | To interrupt request handling and redirect to a configured url | | [rejected](./kibana-plugin-server.onrequesttoolkit.rejected.md) | (error: Error, options?: {`

` statusCode?: number;`

` }) => OnRequestResult | Fail the request with specified error. | +| [setUrl](./kibana-plugin-server.onrequesttoolkit.seturl.md) | (newUrl: string | Url) => void | Change url for an incoming request. | diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.seturl.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.seturl.md new file mode 100644 index 0000000000000..0f20cbdb18d96 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.seturl.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [setUrl](./kibana-plugin-server.onrequesttoolkit.seturl.md) + +## OnRequestToolkit.setUrl property + +Change url for an incoming request. + +Signature: + +```typescript +setUrl: (newUrl: string | Url) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md index 65acef7b54774..0ca7fa2a88294 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md @@ -10,5 +10,7 @@ http: { registerAuth: HttpServiceSetup['registerAuth']; registerOnRequest: HttpServiceSetup['registerOnRequest']; + getBasePathFor: HttpServiceSetup['getBasePathFor']; + setBasePathFor: HttpServiceSetup['setBasePathFor']; }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md index 4aa13c2cda9f1..8878edb18230f 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md @@ -17,5 +17,5 @@ export interface PluginSetupContext | Property | Type | Description | | --- | --- | --- | | [elasticsearch](./kibana-plugin-server.pluginsetupcontext.elasticsearch.md) | {`

` adminClient$: Observable<ClusterClient>;`

` dataClient$: Observable<ClusterClient>;`

` } | | -| [http](./kibana-plugin-server.pluginsetupcontext.http.md) | {`

` registerAuth: HttpServiceSetup['registerAuth'];`

` registerOnRequest: HttpServiceSetup['registerOnRequest'];`

` } | | +| [http](./kibana-plugin-server.pluginsetupcontext.http.md) | {`

` registerAuth: HttpServiceSetup['registerAuth'];`

` registerOnRequest: HttpServiceSetup['registerOnRequest'];`

` getBasePathFor: HttpServiceSetup['getBasePathFor'];`

` setBasePathFor: HttpServiceSetup['setBasePathFor'];`

` } | | diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.contracts.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.contracts.md new file mode 100644 index 0000000000000..090693a814c1d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.contracts.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) > [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) + +## PluginsServiceSetup.contracts property + +Signature: + +```typescript +contracts: Map; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md new file mode 100644 index 0000000000000..c1b3c739d80fe --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md @@ -0,0 +1,20 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) + +## PluginsServiceSetup interface + + +Signature: + +```typescript +export interface PluginsServiceSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) | Map<PluginName, unknown> | | +| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {`

` public: Map<PluginName, DiscoveredPlugin>;`

` internal: Map<PluginName, DiscoveredPluginInternal>;`

` } | | + diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md new file mode 100644 index 0000000000000..ff63944fdf64d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md @@ -0,0 +1,14 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) > [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) + +## PluginsServiceSetup.uiPlugins property + +Signature: + +```typescript +uiPlugins: { + public: Map; + internal: Map; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicestart.contracts.md b/docs/development/core/server/kibana-plugin-server.pluginsservicestart.contracts.md new file mode 100644 index 0000000000000..f6316e7772648 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicestart.contracts.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) > [contracts](./kibana-plugin-server.pluginsservicestart.contracts.md) + +## PluginsServiceStart.contracts property + +Signature: + +```typescript +contracts: Map; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicestart.md b/docs/development/core/server/kibana-plugin-server.pluginsservicestart.md new file mode 100644 index 0000000000000..6e7354d3ea409 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicestart.md @@ -0,0 +1,19 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) + +## PluginsServiceStart interface + + +Signature: + +```typescript +export interface PluginsServiceStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [contracts](./kibana-plugin-server.pluginsservicestart.contracts.md) | Map<PluginName, unknown> | | + diff --git a/package.json b/package.json index f92c6a0af54e1..7fffad48ce437 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "@babel/core": "^7.3.4", "@babel/polyfill": "^7.2.5", "@babel/register": "^7.0.0", + "@elastic/charts": "^3.11.2", "@elastic/datemath": "5.0.2", "@elastic/eui": "10.4.0", "@elastic/filesaver": "1.1.2", @@ -124,7 +125,7 @@ "@types/recompose": "^0.30.5", "JSONStream": "1.1.1", "abortcontroller-polyfill": "^1.1.9", - "angular": "npm:@elastic/angular@1.6.9-kibana.0", + "angular": "1.6.9", "angular-aria": "1.6.6", "angular-elastic": "2.5.0", "angular-recursion": "^1.0.5", diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 94c3864ebfd38..9ea1817e0f49b 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -151,6 +151,7 @@ exports.Cluster = class Cluster { * @param {String} installPath * @param {Object} options * @property {Array} options.esArgs + * @property {String} options.password - super user password used to bootstrap * @returns {Promise} */ async start(installPath, options = {}) { diff --git a/packages/kbn-es/src/install/archive.js b/packages/kbn-es/src/install/archive.js index ff73ce43394b4..11783df16b771 100644 --- a/packages/kbn-es/src/install/archive.js +++ b/packages/kbn-es/src/install/archive.js @@ -62,13 +62,11 @@ exports.installArchive = async function installArchive(archive, options = {}) { await decompress(dest, installPath); log.info('extracted to %s', chalk.bold(installPath)); - if (license === 'trial') { + if (license !== 'oss') { // starting in 6.3, security is disabled by default. Since we bootstrap // the keystore, we can enable security ourselves. await appendToConfig(installPath, 'xpack.security.enabled', 'true'); - } - if (license !== 'oss') { await appendToConfig(installPath, 'xpack.license.self_generated.type', license); await configureKeystore(installPath, password, log); } diff --git a/packages/kbn-interpreter/src/common/index.d.ts b/packages/kbn-interpreter/src/common/index.d.ts new file mode 100644 index 0000000000000..b41f9f318a13b --- /dev/null +++ b/packages/kbn-interpreter/src/common/index.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { Registry } from './lib/registry'; diff --git a/src/legacy/ui/public/kfetch/kfetch_abortable.ts b/packages/kbn-interpreter/src/common/lib/registry.d.ts similarity index 57% rename from src/legacy/ui/public/kfetch/kfetch_abortable.ts rename to packages/kbn-interpreter/src/common/lib/registry.d.ts index 11057054f330f..dccaa22754222 100644 --- a/src/legacy/ui/public/kfetch/kfetch_abortable.ts +++ b/packages/kbn-interpreter/src/common/lib/registry.d.ts @@ -17,29 +17,20 @@ * under the License. */ -import { kfetch, KFetchKibanaOptions, KFetchOptions } from './kfetch'; +export class Registry { + constructor(prop?: string); -type Omit = Pick>; + public wrapper(obj: ItemSpec): Item; -function createAbortable() { - const abortController = new AbortController(); - const { signal, abort } = abortController; + public register(fn: () => ItemSpec): void; - return { - signal, - abort: abort.bind(abortController), - }; -} + public toJS(): { [key: string]: any }; + + public toArray(): Item[]; + + public get(name: string): Item; + + public getProp(): string; -export function kfetchAbortable( - fetchOptions?: Omit, - kibanaOptions?: KFetchKibanaOptions -) { - const { signal, abort } = createAbortable(); - const fetching = kfetch({ ...fetchOptions, signal }, kibanaOptions); - - return { - fetching, - abort, - }; + public reset(): void; } diff --git a/packages/kbn-interpreter/tsconfig.json b/packages/kbn-interpreter/tsconfig.json new file mode 100644 index 0000000000000..63376a7ca1ae8 --- /dev/null +++ b/packages/kbn-interpreter/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["index.d.ts", "src/**/*.d.ts"] +} diff --git a/packages/kbn-test/src/es/es_test_cluster.js b/packages/kbn-test/src/es/es_test_cluster.js index 330a49868b9fb..c8db170cc59e1 100644 --- a/packages/kbn-test/src/es/es_test_cluster.js +++ b/packages/kbn-test/src/es/es_test_cluster.js @@ -80,6 +80,7 @@ export function createEsTestCluster(options = {}) { } await cluster.start(installPath, { + password: config.password, esArgs: [ `cluster.name=${clusterName}`, `http.port=${port}`, diff --git a/packages/kbn-ui-framework/dist/kui_dark.css b/packages/kbn-ui-framework/dist/kui_dark.css index 0565eeebba03c..7a962b510fc0a 100644 --- a/packages/kbn-ui-framework/dist/kui_dark.css +++ b/packages/kbn-ui-framework/dist/kui_dark.css @@ -56,10 +56,6 @@ * 4. Fix an IE bug which causes the last child to overflow the container. * 5. Fixing this bug means we now need to align the children to the right. */ -:focus:not([class^="eui"]) { - -webkit-animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards focusRingAnimate; - animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards focusRingAnimate; } - /** * 1. Required for IE11. */ diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index c4ebe4cc3c674..bfd05c34b8bc1 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -252,9 +252,13 @@ export default class ClusterManager { } shouldRedirectFromOldBasePath(path) { + // strip `s/{id}` prefix when checking for need to redirect + if (path.startsWith('s/')) { + path = path.split('/').slice(2).join('/'); + } + const isApp = path.startsWith('app/'); const isKnownShortPath = ['login', 'logout', 'status'].includes(path); - return isApp || isKnownShortPath; } diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 8db9a83081c17..b0d37f0103df0 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -131,13 +131,12 @@ describe('constructor', () => { expect(FatalErrorsServiceConstructor).toHaveBeenCalledTimes(1); - expect(FatalErrorsServiceConstructor).toHaveBeenLastCalledWith({ + expect(FatalErrorsServiceConstructor).toHaveBeenLastCalledWith( rootDomElement, - injectedMetadata: MockInjectedMetadataService, - stopCoreSystem: expect.any(Function), - }); + expect.any(Function) + ); - const [{ stopCoreSystem }] = FatalErrorsServiceConstructor.mock.calls[0]; + const [, stopCoreSystem] = FatalErrorsServiceConstructor.mock.calls[0]; expect(coreSystem.stop).not.toHaveBeenCalled(); stopCoreSystem(); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 925c6186ea31f..b0101c2ad6d5e 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -22,7 +22,7 @@ import './core.css'; import { CoreSetup, CoreStart } from '.'; import { BasePathService } from './base_path'; import { ChromeService } from './chrome'; -import { FatalErrorsService } from './fatal_errors'; +import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors'; import { HttpService } from './http'; import { I18nService } from './i18n'; import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata'; @@ -68,7 +68,7 @@ export class CoreSystem { private readonly application: ApplicationService; private readonly rootDomElement: HTMLElement; - private readonly overlayTargetDomElement: HTMLDivElement; + private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { const { @@ -87,20 +87,16 @@ export class CoreSystem { injectedMetadata, }); - this.fatalErrors = new FatalErrorsService({ - rootDomElement, - injectedMetadata: this.injectedMetadata, - stopCoreSystem: () => { - this.stop(); - }, + this.fatalErrors = new FatalErrorsService(rootDomElement, () => { + // Stop Core before rendering any fatal errors into the DOM + this.stop(); }); this.notifications = new NotificationsService(); this.http = new HttpService(); this.basePath = new BasePathService(); this.uiSettings = new UiSettingsService(); - this.overlayTargetDomElement = document.createElement('div'); - this.overlay = new OverlayService(this.overlayTargetDomElement); + this.overlay = new OverlayService(); this.application = new ApplicationService(); this.chrome = new ChromeService({ browserSupportsCsp }); @@ -115,11 +111,17 @@ export class CoreSystem { public async setup() { try { + // Setup FatalErrorsService and it's dependencies first so that we're + // able to render any errors. const i18n = this.i18n.setup(); const injectedMetadata = this.injectedMetadata.setup(); - const fatalErrors = this.fatalErrors.setup({ i18n }); - const http = this.http.setup({ fatalErrors }); + this.fatalErrorsSetup = this.fatalErrors.setup({ injectedMetadata, i18n }); const basePath = this.basePath.setup({ injectedMetadata }); + const http = this.http.setup({ + basePath, + injectedMetadata, + fatalErrors: this.fatalErrorsSetup, + }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata, @@ -136,7 +138,7 @@ export class CoreSystem { application, basePath, chrome, - fatalErrors, + fatalErrors: this.fatalErrorsSetup, http, i18n, injectedMetadata, @@ -148,9 +150,15 @@ export class CoreSystem { await this.plugins.setup(core); await this.legacyPlatform.setup({ core }); - return { fatalErrors }; + return { fatalErrors: this.fatalErrorsSetup }; } catch (error) { - this.fatalErrors.add(error); + if (this.fatalErrorsSetup) { + this.fatalErrorsSetup.add(error); + } else { + // If the FatalErrorsService has not yet been setup, log error to console + // eslint-disable-next-line no-console + console.log(error); + } } } @@ -158,10 +166,12 @@ export class CoreSystem { try { const injectedMetadata = await this.injectedMetadata.start(); const basePath = await this.basePath.start({ injectedMetadata }); + const http = await this.http.start(); const i18n = await this.i18n.start(); const application = await this.application.start({ basePath, injectedMetadata }); const notificationsTargetDomElement = document.createElement('div'); + const overlayTargetDomElement = document.createElement('div'); const legacyPlatformTargetDomElement = document.createElement('div'); // ensure the rootDomElement is empty @@ -169,17 +179,18 @@ export class CoreSystem { this.rootDomElement.classList.add('coreSystemRootDomElement'); this.rootDomElement.appendChild(notificationsTargetDomElement); this.rootDomElement.appendChild(legacyPlatformTargetDomElement); - this.rootDomElement.appendChild(this.overlayTargetDomElement); + this.rootDomElement.appendChild(overlayTargetDomElement); const notifications = await this.notifications.start({ i18n, targetDomElement: notificationsTargetDomElement, }); - const overlays = await this.overlay.start({ i18n }); + const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement }); const core: CoreStart = { application, basePath, + http, i18n, injectedMetadata, notifications, @@ -189,7 +200,13 @@ export class CoreSystem { await this.plugins.start(core); await this.legacyPlatform.start({ core, targetDomElement: legacyPlatformTargetDomElement }); } catch (error) { - this.fatalErrors.add(error); + if (this.fatalErrorsSetup) { + this.fatalErrorsSetup.add(error); + } else { + // If the FatalErrorsService has not yet been setup, log error to console + // eslint-disable-next-line no-console + console.error(error); + } } } diff --git a/src/core/public/fatal_errors/__snapshots__/fatal_errors_service.test.ts.snap b/src/core/public/fatal_errors/__snapshots__/fatal_errors_service.test.ts.snap index 59f3de3e2b907..df60b228972fc 100644 --- a/src/core/public/fatal_errors/__snapshots__/fatal_errors_service.test.ts.snap +++ b/src/core/public/fatal_errors/__snapshots__/fatal_errors_service.test.ts.snap @@ -1,32 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`#add() deletes all children of rootDomElement and renders into it: fatal error screen component 1`] = ` -Array [ - Array [ - - - , -

, - ], -] -`; - -exports[`#add() deletes all children of rootDomElement and renders into it: fatal error screen container 1`] = ` -
-
-
-`; - -exports[`setup.add() deletes all children of rootDomElement and renders into it: fatal error screen component 1`] = ` Array [ Array [ @@ -36,7 +14,7 @@ Array [ ] `; -exports[`setup.add() deletes all children of rootDomElement and renders into it: fatal error screen container 1`] = ` +exports[`#add() deletes all children of rootDomElement and renders into it: fatal error screen container 1`] = `
diff --git a/src/core/public/fatal_errors/fatal_errors_screen.tsx b/src/core/public/fatal_errors/fatal_errors_screen.tsx index 551e54d3003e4..f7184b01a0a78 100644 --- a/src/core/public/fatal_errors/fatal_errors_screen.tsx +++ b/src/core/public/fatal_errors/fatal_errors_screen.tsx @@ -33,16 +33,16 @@ import { tap } from 'rxjs/operators'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ErrorInfo } from './get_error_info'; +import { FatalErrorInfo } from './get_error_info'; interface Props { kibanaVersion: string; buildNumber: number; - errorInfo$: Rx.Observable; + errorInfo$: Rx.Observable; } interface State { - errors: ErrorInfo[]; + errors: FatalErrorInfo[]; } export class FatalErrorsScreen extends React.Component { diff --git a/src/core/public/fatal_errors/fatal_errors_service.mock.ts b/src/core/public/fatal_errors/fatal_errors_service.mock.ts index 89bcc46f0372f..dd7702a7ee7dd 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.mock.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.mock.ts @@ -31,7 +31,6 @@ type FatalErrorsServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), - add: jest.fn(() => undefined as never), }; mocked.setup.mockReturnValue(createSetupContractMock()); diff --git a/src/core/public/fatal_errors/fatal_errors_service.test.ts b/src/core/public/fatal_errors/fatal_errors_service.test.ts index b1ad92c8c2f62..373b0efddc2cf 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.test.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.test.ts @@ -25,16 +25,14 @@ expect.addSnapshotSerializer({ }); import { mockRender } from './fatal_errors_service.test.mocks'; +import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { FatalErrorsService } from './fatal_errors_service'; function setupService() { const rootDomElement = document.createElement('div'); - const injectedMetadata = { - getKibanaBuildNumber: jest.fn().mockReturnValue('kibanaBuildNumber'), - getKibanaVersion: jest.fn().mockReturnValue('kibanaVersion'), - }; + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); const stopCoreSystem = jest.fn(); @@ -44,16 +42,13 @@ function setupService() { }, }; + const fatalErrorsService = new FatalErrorsService(rootDomElement, stopCoreSystem); + return { rootDomElement, injectedMetadata, - i18n, stopCoreSystem, - fatalErrors: new FatalErrorsService({ - injectedMetadata: injectedMetadata as any, - rootDomElement, - stopCoreSystem, - }), + fatalErrors: fatalErrorsService.setup({ injectedMetadata, i18n }), }; } @@ -91,46 +86,12 @@ describe('#add()', () => { }); }); -describe('setup.add()', () => { - it('exposes a function that passes its two arguments to fatalErrors.add()', () => { - const { fatalErrors, i18n } = setupService(); - - jest.spyOn(fatalErrors, 'add').mockImplementation(() => undefined as never); - - expect(fatalErrors.add).not.toHaveBeenCalled(); - const { add } = fatalErrors.setup({ i18n }); - add('foo', 'bar'); - expect(fatalErrors.add).toHaveBeenCalledTimes(1); - expect(fatalErrors.add).toHaveBeenCalledWith('foo', 'bar'); - }); - - it('deletes all children of rootDomElement and renders into it', () => { - const { fatalErrors, i18n, rootDomElement } = setupService(); - - rootDomElement.innerHTML = ` -

Loading...

-
- `; - - expect(mockRender).not.toHaveBeenCalled(); - expect(rootDomElement.children).toHaveLength(2); - - const { add } = fatalErrors.setup({ i18n }); - - expect(() => add(new Error('foo'))).toThrowError(); - expect(rootDomElement).toMatchSnapshot('fatal error screen container'); - expect(mockRender.mock.calls).toMatchSnapshot('fatal error screen component'); - }); -}); - describe('setup.get$()', () => { it('provides info about the errors passed to fatalErrors.add()', () => { - const { fatalErrors, i18n } = setupService(); - - const setup = fatalErrors.setup({ i18n }); + const { fatalErrors } = setupService(); const onError = jest.fn(); - setup.get$().subscribe(onError); + fatalErrors.get$().subscribe(onError); expect(onError).not.toHaveBeenCalled(); expect(() => { diff --git a/src/core/public/fatal_errors/fatal_errors_service.tsx b/src/core/public/fatal_errors/fatal_errors_service.tsx index e263c8012f477..92c7633cfb9f1 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.tsx +++ b/src/core/public/fatal_errors/fatal_errors_service.tsx @@ -23,18 +23,13 @@ import * as Rx from 'rxjs'; import { first, tap } from 'rxjs/operators'; import { I18nSetup } from '../i18n'; -import { InjectedMetadataService } from '../injected_metadata'; +import { InjectedMetadataSetup } from '../'; import { FatalErrorsScreen } from './fatal_errors_screen'; -import { ErrorInfo, getErrorInfo } from './get_error_info'; - -export interface FatalErrorsParams { - rootDomElement: HTMLElement; - injectedMetadata: InjectedMetadataService; - stopCoreSystem: () => void; -} +import { FatalErrorInfo, getErrorInfo } from './get_error_info'; interface Deps { i18n: I18nSetup; + injectedMetadata: InjectedMetadataSetup; } /** @@ -56,47 +51,51 @@ export interface FatalErrorsSetup { /** * An Observable that will emit whenever a fatal error is added with `add()` */ - get$: () => Rx.Observable; + get$: () => Rx.Observable; } /** @interal */ export class FatalErrorsService { - private readonly errorInfo$ = new Rx.ReplaySubject(); - private i18n?: I18nSetup; + private readonly errorInfo$ = new Rx.ReplaySubject(); + + /** + * + * @param rootDomElement + * @param onFirstErrorCb - Callback function that gets executed after the first error, + * but before the FatalErrorsService renders the error to the DOM. + */ + constructor(private rootDomElement: HTMLElement, private onFirstErrorCb: () => void) {} - constructor(private params: FatalErrorsParams) { + public setup({ i18n, injectedMetadata }: Deps) { this.errorInfo$ .pipe( first(), - tap(() => this.onFirstError()) + tap(() => { + this.onFirstErrorCb(); + this.renderError(injectedMetadata, i18n); + }) ) .subscribe({ error: error => { // eslint-disable-next-line no-console - console.error('Uncaught error in fatal error screen internals', error); + console.error('Uncaught error in fatal error service internals', error); }, }); - } - - public add: FatalErrorsSetup['add'] = (error, source?) => { - const errorInfo = getErrorInfo(error, source); - this.errorInfo$.next(errorInfo); - - if (error instanceof Error) { - // make stack traces clickable by putting whole error in the console - // eslint-disable-next-line no-console - console.error(error); - } + const fatalErrorsSetup: FatalErrorsSetup = { + add: (error, source?) => { + const errorInfo = getErrorInfo(error, source); - throw error; - }; + this.errorInfo$.next(errorInfo); - public setup({ i18n }: Deps) { - this.i18n = i18n; + if (error instanceof Error) { + // make stack traces clickable by putting whole error in the console + // eslint-disable-next-line no-console + console.error(error); + } - const fatalErrorsSetup: FatalErrorsSetup = { - add: this.add, + throw error; + }, get$: () => { return this.errorInfo$.asObservable(); }, @@ -105,30 +104,22 @@ export class FatalErrorsService { return fatalErrorsSetup; } - private onFirstError() { - // stop the core systems so that things like the legacy platform are stopped - // and angular/react components are unmounted; - this.params.stopCoreSystem(); - + private renderError(injectedMetadata: InjectedMetadataSetup, i18n: I18nSetup) { // delete all content in the rootDomElement - this.params.rootDomElement.textContent = ''; + this.rootDomElement.textContent = ''; // create and mount a container for the const container = document.createElement('div'); - this.params.rootDomElement.appendChild(container); - - // If error occurred before I18nService has been set up we don't have any - // i18n context to provide. - const I18nContext = this.i18n ? this.i18n.Context : React.Fragment; + this.rootDomElement.appendChild(container); render( - + - , + , container ); } diff --git a/src/core/public/fatal_errors/get_error_info.ts b/src/core/public/fatal_errors/get_error_info.ts index b44a0a0ca99c5..0c8bdb374ce63 100644 --- a/src/core/public/fatal_errors/get_error_info.ts +++ b/src/core/public/fatal_errors/get_error_info.ts @@ -22,7 +22,7 @@ import { inspect } from 'util'; /** * Produce a string version of an error, */ -function formatErrorMessage(error: any) { +function formatErrorMessage(error: any): string { if (typeof error === 'string') { return error; } @@ -64,10 +64,10 @@ function formatStack(err: Error) { } /** - * Produce a simple ErrorInfo object from some error and optional source, used for + * Produce a simple FatalErrorInfo object from some error and optional source, used for * displaying error information on the fatal error screen */ -export function getErrorInfo(error: any, source?: string) { +export function getErrorInfo(error: any, source?: string): FatalErrorInfo { const prefix = source ? source + ': ' : ''; return { message: prefix + formatErrorMessage(error), @@ -75,4 +75,12 @@ export function getErrorInfo(error: any, source?: string) { }; } -export type ErrorInfo = ReturnType; +/** + * Represents the `message` and `stack` of a fatal Error + * + * @public + * */ +export interface FatalErrorInfo { + message: string; + stack: string | undefined; +} diff --git a/src/core/public/fatal_errors/index.ts b/src/core/public/fatal_errors/index.ts index 71ca5bf71ee2a..e37a36152cf91 100644 --- a/src/core/public/fatal_errors/index.ts +++ b/src/core/public/fatal_errors/index.ts @@ -18,3 +18,4 @@ */ export { FatalErrorsSetup, FatalErrorsService } from './fatal_errors_service'; +export { FatalErrorInfo } from './get_error_info'; diff --git a/src/core/public/http/_import_objects.ndjson b/src/core/public/http/_import_objects.ndjson new file mode 100644 index 0000000000000..3511fb44cdfb2 --- /dev/null +++ b/src/core/public/http/_import_objects.ndjson @@ -0,0 +1 @@ +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Log Agents","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"082f1d60-a2e7-11e7-bb30-233be9be6a15","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1} diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts new file mode 100644 index 0000000000000..9bfc13820cb55 --- /dev/null +++ b/src/core/public/http/fetch.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { merge } from 'lodash'; +import { format } from 'url'; + +import { HttpFetchOptions, HttpBody, Deps } from './types'; +import { HttpFetchError } from './http_fetch_error'; + +const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; +const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; + +export const setup = ({ basePath, injectedMetadata }: Deps) => { + async function fetch(path: string, options: HttpFetchOptions = {}): Promise { + const { query, prependBasePath, ...fetchOptions } = merge( + { + method: 'GET', + credentials: 'same-origin', + prependBasePath: true, + headers: { + 'kbn-version': injectedMetadata.getKibanaVersion(), + 'Content-Type': 'application/json', + }, + }, + options + ); + const url = format({ + pathname: prependBasePath ? basePath.addToPath(path) : path, + query, + }); + + if ( + options.headers && + 'Content-Type' in options.headers && + options.headers['Content-Type'] === undefined + ) { + delete fetchOptions.headers['Content-Type']; + } + + let response; + let body = null; + + try { + response = await window.fetch(url, fetchOptions as RequestInit); + } catch (err) { + throw new HttpFetchError(err.message); + } + + const contentType = response.headers.get('Content-Type') || ''; + + try { + if (NDJSON_CONTENT.test(contentType)) { + body = await response.blob(); + } else if (JSON_CONTENT.test(contentType)) { + body = await response.json(); + } else { + body = await response.text(); + } + } catch (err) { + throw new HttpFetchError(err.message, response, body); + } + + if (!response.ok) { + throw new HttpFetchError(response.statusText, response, body); + } + + return body; + } + + function shorthand(method: string) { + return (path: string, options: HttpFetchOptions = {}) => fetch(path, { ...options, method }); + } + + return { fetch, shorthand }; +}; diff --git a/src/core/public/http/http_fetch_error.ts b/src/core/public/http/http_fetch_error.ts new file mode 100644 index 0000000000000..a73fb7e3ffbd4 --- /dev/null +++ b/src/core/public/http/http_fetch_error.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export class HttpFetchError extends Error { + constructor(message: string, public readonly response?: Response, public readonly body?: any) { + super(message); + + // captureStackTrace is only available in the V8 engine, so any browser using + // a different JS engine won't have access to this method. + if (Error.captureStackTrace) { + Error.captureStackTrace(this, HttpFetchError); + } + } +} diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 3146ad4d2217f..7893d250f0812 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -16,27 +16,30 @@ * specific language governing permissions and limitations * under the License. */ -import { HttpService, HttpSetup } from './http_service'; -const createSetupContractMock = () => { - const setupContract: jest.Mocked = { - addLoadingCount: jest.fn(), - getLoadingCount$: jest.fn(), - }; - return setupContract; -}; +import { HttpService, HttpSetup, HttpStart } from './http_service'; -type HttpServiceContract = PublicMethodsOf; -const createMock = () => { - const mocked: jest.Mocked = { - setup: jest.fn(), - stop: jest.fn(), - }; - mocked.setup.mockReturnValue(createSetupContractMock()); - return mocked; -}; +const createSetupContractMock = (): jest.Mocked => ({ + fetch: jest.fn(), + get: jest.fn(), + head: jest.fn(), + post: jest.fn(), + put: jest.fn(), + patch: jest.fn(), + delete: jest.fn(), + options: jest.fn(), + addLoadingCount: jest.fn(), + getLoadingCount$: jest.fn(), +}); +const createStartContractMock = (): jest.Mocked => undefined; +const createMock = (): jest.Mocked> => ({ + setup: jest.fn().mockReturnValue(createSetupContractMock()), + start: jest.fn().mockReturnValue(createStartContractMock()), + stop: jest.fn(), +}); export const httpServiceMock = { create: createMock, createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 35b674aca9b9f..51f4af44d313d 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -19,35 +19,254 @@ import * as Rx from 'rxjs'; import { toArray } from 'rxjs/operators'; +// @ts-ignore +import fetchMock from 'fetch-mock/es5/client'; +import { BasePathService } from '../base_path/base_path_service'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { HttpService } from './http_service'; +import { readFileSync } from 'fs'; +import { join } from 'path'; function setupService() { - const service = new HttpService(); + const httpService = new HttpService(); const fatalErrors = fatalErrorsServiceMock.createSetupContract(); - const setup = service.setup({ fatalErrors }); + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); - return { service, fatalErrors, setup }; + injectedMetadata.getBasePath.mockReturnValueOnce('http://localhost/myBase'); + + const basePath = new BasePathService().setup({ injectedMetadata }); + const http = httpService.setup({ basePath, fatalErrors, injectedMetadata }); + + return { httpService, fatalErrors, http }; } +describe('http requests', async () => { + afterEach(() => { + fetchMock.restore(); + }); + + it('should use supplied request method', async () => { + const { http } = setupService(); + + fetchMock.post('*', {}); + await http.fetch('/my/path', { method: 'POST' }); + + expect(fetchMock.lastOptions()!.method).toBe('POST'); + }); + + it('should use supplied Content-Type', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('/my/path', { headers: { 'Content-Type': 'CustomContentType' } }); + + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'Content-Type': 'CustomContentType', + }); + }); + + it('should use supplied pathname and querystring', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('/my/path', { query: { a: 'b' } }); + + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); + }); + + it('should use supplied headers', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('/my/path', { + headers: { myHeader: 'foo' }, + }); + + expect(fetchMock.lastOptions()!.headers).toEqual({ + 'Content-Type': 'application/json', + 'kbn-version': 'kibanaVersion', + myHeader: 'foo', + }); + }); + + it('should return response', async () => { + const { http } = setupService(); + + fetchMock.get('*', { foo: 'bar' }); + + const json = await http.fetch('/my/path'); + + expect(json).toEqual({ foo: 'bar' }); + }); + + it('should prepend url with basepath by default', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('/my/path'); + + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); + }); + + it('should not prepend url with basepath when disabled', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('my/path', { prependBasePath: false }); + + expect(fetchMock.lastUrl()).toBe('/my/path'); + }); + + it('should make request with defaults', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('/my/path'); + + expect(fetchMock.lastOptions()!).toMatchObject({ + method: 'GET', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + 'kbn-version': 'kibanaVersion', + }, + }); + }); + + it('should reject on network error', async () => { + const { http } = setupService(); + + expect.assertions(1); + fetchMock.get('*', { status: 500 }); + + await expect(http.fetch('/my/path')).rejects.toThrow(/Internal Server Error/); + }); + + it('should contain error message when throwing response', async () => { + const { http } = setupService(); + + fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); + + await expect(http.fetch('/my/path')).rejects.toMatchObject({ + message: 'Not Found', + body: { + foo: 'bar', + }, + response: { + status: 404, + url: 'http://localhost/myBase/my/path', + }, + }); + }); + + it('should support get() helper', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.get('/my/path', { method: 'POST' }); + + expect(fetchMock.lastOptions()!.method).toBe('GET'); + }); + + it('should support head() helper', async () => { + const { http } = setupService(); + + fetchMock.head('*', {}); + await http.head('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('HEAD'); + }); + + it('should support post() helper', async () => { + const { http } = setupService(); + + fetchMock.post('*', {}); + await http.post('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('POST'); + }); + + it('should support put() helper', async () => { + const { http } = setupService(); + + fetchMock.put('*', {}); + await http.put('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('PUT'); + }); + + it('should support patch() helper', async () => { + const { http } = setupService(); + + fetchMock.patch('*', {}); + await http.patch('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('PATCH'); + }); + + it('should support delete() helper', async () => { + const { http } = setupService(); + + fetchMock.delete('*', {}); + await http.delete('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('DELETE'); + }); + + it('should support options() helper', async () => { + const { http } = setupService(); + + fetchMock.mock('*', { method: 'OPTIONS' }); + await http.options('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('OPTIONS'); + }); + + it('should make requests for NDJSON content', async () => { + const { http } = setupService(); + const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); + const body = new FormData(); + + body.append('file', content); + fetchMock.post('*', { + body: content, + headers: { 'Content-Type': 'application/ndjson' }, + }); + + const data = await http.post('/my/path', { + body, + headers: { + 'Content-Type': undefined, + }, + }); + + expect(data).toBeInstanceOf(Blob); + + const ndjson = await new Response(data).text(); + + expect(ndjson).toEqual(content); + }); +}); + describe('addLoadingCount()', async () => { it('subscribes to passed in sources, unsubscribes on stop', () => { - const { service, setup } = setupService(); + const { httpService, http } = setupService(); const unsubA = jest.fn(); const subA = jest.fn().mockReturnValue(unsubA); - setup.addLoadingCount(new Rx.Observable(subA)); + http.addLoadingCount(new Rx.Observable(subA)); expect(subA).toHaveBeenCalledTimes(1); expect(unsubA).not.toHaveBeenCalled(); const unsubB = jest.fn(); const subB = jest.fn().mockReturnValue(unsubB); - setup.addLoadingCount(new Rx.Observable(subB)); + http.addLoadingCount(new Rx.Observable(subB)); expect(subB).toHaveBeenCalledTimes(1); expect(unsubB).not.toHaveBeenCalled(); - service.stop(); + httpService.stop(); expect(subA).toHaveBeenCalledTimes(1); expect(unsubA).toHaveBeenCalledTimes(1); @@ -56,35 +275,35 @@ describe('addLoadingCount()', async () => { }); it('adds a fatal error if source observables emit an error', async () => { - const { setup, fatalErrors } = setupService(); + const { http, fatalErrors } = setupService(); - setup.addLoadingCount(Rx.throwError(new Error('foo bar'))); + http.addLoadingCount(Rx.throwError(new Error('foo bar'))); expect(fatalErrors.add.mock.calls).toMatchSnapshot(); }); it('adds a fatal error if source observable emits a negative number', async () => { - const { setup, fatalErrors } = setupService(); + const { http, fatalErrors } = setupService(); - setup.addLoadingCount(Rx.of(1, 2, 3, 4, -9)); + http.addLoadingCount(Rx.of(1, 2, 3, 4, -9)); expect(fatalErrors.add.mock.calls).toMatchSnapshot(); }); }); describe('getLoadingCount$()', async () => { it('emits 0 initially, the right count when sources emit their own count, and ends with zero', async () => { - const { service, setup } = setupService(); + const { httpService, http } = setupService(); const countA$ = new Rx.Subject(); const countB$ = new Rx.Subject(); const countC$ = new Rx.Subject(); - const promise = setup + const promise = http .getLoadingCount$() .pipe(toArray()) .toPromise(); - setup.addLoadingCount(countA$); - setup.addLoadingCount(countB$); - setup.addLoadingCount(countC$); + http.addLoadingCount(countA$); + http.addLoadingCount(countB$); + http.addLoadingCount(countC$); countA$.next(100); countB$.next(10); @@ -94,20 +313,20 @@ describe('getLoadingCount$()', async () => { countC$.complete(); countB$.next(0); - service.stop(); + httpService.stop(); expect(await promise).toMatchSnapshot(); }); it('only emits when loading count changes', async () => { - const { service, setup } = setupService(); + const { httpService, http } = setupService(); const count$ = new Rx.Subject(); - const promise = setup + const promise = http .getLoadingCount$() .pipe(toArray()) .toPromise(); - setup.addLoadingCount(count$); + http.addLoadingCount(count$); count$.next(0); count$.next(0); count$.next(0); @@ -115,7 +334,7 @@ describe('getLoadingCount$()', async () => { count$.next(0); count$.next(1); count$.next(1); - service.stop(); + httpService.stop(); expect(await promise).toMatchSnapshot(); }); diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 4121181cf80ec..a54e6947a1980 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -28,19 +28,26 @@ import { tap, } from 'rxjs/operators'; -import { FatalErrorsSetup } from '../fatal_errors'; - -interface Deps { - fatalErrors: FatalErrorsSetup; -} +import { Deps } from './types'; +import { setup } from './fetch'; /** @internal */ export class HttpService { private readonly loadingCount$ = new Rx.BehaviorSubject(0); private readonly stop$ = new Rx.Subject(); - public setup({ fatalErrors }: Deps) { + public setup(deps: Deps) { + const { fetch, shorthand } = setup(deps); + return { + fetch, + delete: shorthand('DELETE'), + get: shorthand('GET'), + head: shorthand('HEAD'), + options: shorthand('OPTIONS'), + patch: shorthand('PATCH'), + post: shorthand('POST'), + put: shorthand('PUT'), addLoadingCount: (count$: Rx.Observable) => { count$ .pipe( @@ -67,7 +74,7 @@ export class HttpService { this.loadingCount$.next(this.loadingCount$.getValue() + delta); }, error: error => { - fatalErrors.add(error); + deps.fatalErrors.add(error); }, }); }, @@ -78,6 +85,9 @@ export class HttpService { }; } + // eslint-disable-next-line no-unused-params + public start() {} + public stop() { this.stop$.next(); this.loadingCount$.complete(); @@ -86,3 +96,5 @@ export class HttpService { /** @public */ export type HttpSetup = ReturnType; +/** @public */ +export type HttpStart = ReturnType; diff --git a/src/core/public/http/index.ts b/src/core/public/http/index.ts index 24ba49a4dfcac..d7b2efe9b6cd0 100644 --- a/src/core/public/http/index.ts +++ b/src/core/public/http/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { HttpService, HttpSetup } from './http_service'; +export { HttpService, HttpSetup, HttpStart } from './http_service'; +export { HttpFetchError } from './http_fetch_error'; diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts new file mode 100644 index 0000000000000..05f6ab502246d --- /dev/null +++ b/src/core/public/http/types.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BasePathSetup } from '../base_path'; +import { InjectedMetadataSetup } from '../injected_metadata'; +import { FatalErrorsSetup } from '../fatal_errors'; + +export interface HttpHeadersInit { + [name: string]: any; +} +export interface HttpRequestInit { + body?: BodyInit | null; + cache?: RequestCache; + credentials?: RequestCredentials; + headers?: HttpHeadersInit; + integrity?: string; + keepalive?: boolean; + method?: string; + mode?: RequestMode; + redirect?: RequestRedirect; + referrer?: string; + referrerPolicy?: ReferrerPolicy; + signal?: AbortSignal | null; + window?: any; +} +export interface Deps { + basePath: BasePathSetup; + injectedMetadata: InjectedMetadataSetup; + fatalErrors: FatalErrorsSetup; +} +export interface HttpFetchQuery { + [key: string]: string | number | boolean | undefined; +} +export interface HttpFetchOptions extends HttpRequestInit { + query?: HttpFetchQuery; + prependBasePath?: boolean; + headers?: HttpHeadersInit; +} +export type HttpBody = BodyInit | null; diff --git a/src/core/public/i18n/i18n_service.tsx b/src/core/public/i18n/i18n_service.tsx index 6550770da532b..5f26e84064b0c 100644 --- a/src/core/public/i18n/i18n_service.tsx +++ b/src/core/public/i18n/i18n_service.tsx @@ -304,4 +304,7 @@ export interface I18nSetup { Context: ({ children }: { children: React.ReactNode }) => JSX.Element; } +/** + * @public + */ export type I18nStart = I18nSetup; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index a0fda91695cb1..a87f03bfd9720 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -25,8 +25,8 @@ import { ChromeHelpExtension, ChromeSetup, } from './chrome'; -import { FatalErrorsSetup } from './fatal_errors'; -import { HttpSetup } from './http'; +import { FatalErrorsSetup, FatalErrorInfo } from './fatal_errors'; +import { HttpSetup, HttpStart } from './http'; import { I18nSetup, I18nStart } from './i18n'; import { InjectedMetadataParams, @@ -41,15 +41,21 @@ import { ToastsApi, NotificationsStart, } from './notifications'; -import { FlyoutRef, OverlayStart } from './overlays'; -import { Plugin, PluginInitializer, PluginInitializerContext, PluginSetupContext } from './plugins'; +import { OverlayRef, OverlayStart } from './overlays'; +import { + Plugin, + PluginInitializer, + PluginInitializerContext, + PluginSetupContext, + PluginStartContext, +} from './plugins'; import { UiSettingsClient, UiSettingsSetup, UiSettingsState } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; export { CoreContext, CoreSystem } from './core_system'; /** - * Core services exposed to the start lifecycle + * Core services exposed to the setup lifecycle * * @public * @@ -78,11 +84,22 @@ export interface CoreSetup { chrome: ChromeSetup; } +/** + * Core services exposed to the start lifecycle + * + * @public + * + * @internalRemarks We document the properties with \@link tags to improve + * navigation in the generated docs until there's a fix for + * https://github.com/Microsoft/web-build-tools/issues/1237 + */ export interface CoreStart { /** {@link ApplicationStart} */ application: ApplicationStart; /** {@link BasePathStart} */ basePath: BasePathStart; + /** {@link HttpStart} */ + http: HttpStart; /** {@link I18nStart} */ i18n: I18nStart; /** {@link InjectedMetadataStart} */ @@ -99,7 +116,9 @@ export { BasePathSetup, BasePathStart, HttpSetup, + HttpStart, FatalErrorsSetup, + FatalErrorInfo, Capabilities, ChromeSetup, ChromeBadge, @@ -116,10 +135,11 @@ export { PluginInitializer, PluginInitializerContext, PluginSetupContext, + PluginStartContext, NotificationsSetup, NotificationsStart, + OverlayRef, OverlayStart, - FlyoutRef, Toast, ToastInput, ToastsApi, diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 55d959e69d173..39c40cb8a4bfd 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -27,6 +27,7 @@ const createSetupContractMock = () => { getPlugins: jest.fn(), getInjectedVar: jest.fn(), getInjectedVars: jest.fn(), + getKibanaBuildNumber: jest.fn(), }; setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); setupContract.getKibanaVersion.mockReturnValue('kibanaVersion'); @@ -47,8 +48,6 @@ type InjectedMetadataServiceContract = PublicMethodsOf; const createMock = (): jest.Mocked => ({ setup: jest.fn().mockReturnValue(createSetupContractMock()), start: jest.fn().mockReturnValue(createStartContractMock()), - getKibanaVersion: jest.fn(), - getKibanaBuildNumber: jest.fn(), }); export const injectedMetadataServiceMock = { diff --git a/src/core/public/injected_metadata/injected_metadata_service.test.ts b/src/core/public/injected_metadata/injected_metadata_service.test.ts index 74e860d16a2b2..ef35fd2aa78ac 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.test.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.test.ts @@ -20,42 +20,29 @@ import { DiscoveredPlugin } from '../../server'; import { InjectedMetadataService } from './injected_metadata_service'; -describe('#getKibanaVersion', () => { - it('returns version from injectedMetadata', () => { - const injectedMetadata = new InjectedMetadataService({ - injectedMetadata: { - version: 'foo', - }, - } as any); - - expect(injectedMetadata.getKibanaVersion()).toBe('foo'); - }); -}); - -describe('#getKibanaBuildNumber', () => { +describe('setup.getKibanaBuildNumber()', () => { it('returns buildNumber from injectedMetadata', () => { - const injectedMetadata = new InjectedMetadataService({ + const setup = new InjectedMetadataService({ injectedMetadata: { buildNumber: 'foo', }, - } as any); + } as any).setup(); - expect(injectedMetadata.getKibanaBuildNumber()).toBe('foo'); + expect(setup.getKibanaBuildNumber()).toBe('foo'); }); }); describe('setup.getCspConfig()', () => { it('returns injectedMetadata.csp', () => { - const injectedMetadata = new InjectedMetadataService({ + const setup = new InjectedMetadataService({ injectedMetadata: { csp: { warnLegacyBrowsers: true, }, }, - } as any); + } as any).setup(); - const contract = injectedMetadata.setup(); - expect(contract.getCspConfig()).toEqual({ + expect(setup.getCspConfig()).toEqual({ warnLegacyBrowsers: true, }); }); diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 625f29d695274..de7ad1595ed36 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -83,6 +83,10 @@ export class InjectedMetadataService { constructor(private readonly params: InjectedMetadataParams) {} + public start(): InjectedMetadataStart { + return this.setup(); + } + public setup(): InjectedMetadataSetup { return { getBasePath: () => { @@ -90,7 +94,7 @@ export class InjectedMetadataService { }, getKibanaVersion: () => { - return this.getKibanaVersion(); + return this.state.version; }, getCspConfig: () => { @@ -112,19 +116,11 @@ export class InjectedMetadataService { getInjectedVars: () => { return this.state.vars; }, - }; - } - - public start(): InjectedMetadataStart { - return this.setup(); - } - public getKibanaVersion() { - return this.state.version; - } - - public getKibanaBuildNumber() { - return this.state.buildNumber; + getKibanaBuildNumber: () => { + return this.state.buildNumber; + }, + }; } } @@ -135,6 +131,7 @@ export class InjectedMetadataService { */ export interface InjectedMetadataSetup { getBasePath: () => string; + getKibanaBuildNumber: () => number; getKibanaVersion: () => string; getCspConfig: () => { warnLegacyBrowsers: boolean; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 78c159304eda8..4007a8128f65c 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -194,6 +194,7 @@ const defaultSetupDeps = { const applicationStart = applicationServiceMock.createStartContract(); const basePathStart = basePathServiceMock.createStartContract(); const i18nStart = i18nServiceMock.createStartContract(); +const httpStart = httpServiceMock.createStartContract(); const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); const notificationsStart = notificationServiceMock.createStartContract(); const overlayStart = overlayServiceMock.createStartContract(); @@ -203,6 +204,7 @@ const defaultStartDeps = { application: applicationStart, basePath: basePathStart, i18n: i18nStart, + http: httpStart, injectedMetadata: injectedMetadataStart, notifications: notificationsStart, overlays: overlayStart, diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index bc1aff696ed63..6e6f256497426 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -70,6 +70,7 @@ export class LegacyPlatformService { require('ui/metadata').__newPlatformSetup__(injectedMetadata.getLegacyMetadata()); require('ui/i18n').__newPlatformSetup__(i18n.Context); require('ui/notify/fatal_error').__newPlatformSetup__(fatalErrors); + require('ui/kfetch').__newPlatformSetup__(http); require('ui/notify/toasts').__newPlatformSetup__(notifications.toasts); require('ui/chrome/api/loading_count').__newPlatformSetup__(http); require('ui/chrome/api/base_path').__newPlatformSetup__(basePath); diff --git a/src/core/public/notifications/notifications_service.ts b/src/core/public/notifications/notifications_service.ts index 09a527e90ae38..d68d848c1d88a 100644 --- a/src/core/public/notifications/notifications_service.ts +++ b/src/core/public/notifications/notifications_service.ts @@ -64,7 +64,9 @@ export class NotificationsService { const toastsContainer = document.createElement('div'); targetDomElement.appendChild(toastsContainer); - return { toasts: this.toasts.start({ i18n: i18nDep, targetDomElement: toastsContainer }) }; + return { + toasts: this.toasts.start({ i18n: i18nDep, targetDomElement: toastsContainer }), + }; } public stop() { diff --git a/src/core/public/overlays/__snapshots__/modal.test.tsx.snap b/src/core/public/overlays/__snapshots__/modal.test.tsx.snap new file mode 100644 index 0000000000000..a4e6f5d6f72b8 --- /dev/null +++ b/src/core/public/overlays/__snapshots__/modal.test.tsx.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalService ModalRef#close() can be called multiple times on the same ModalRef 1`] = ` +Array [ + Array [ +
, + ], +] +`; + +exports[`ModalService openModal() renders a modal to the DOM 1`] = ` +Array [ + Array [ + + + + + Modal content + + + + , +
, + ], +] +`; + +exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = ` +Array [ + Array [ + + + + + Modal content 1 + + + + , +
, + ], + Array [ + + + + + Flyout content 2 + + + + , +
, + ], +] +`; diff --git a/src/core/public/overlays/flyout.tsx b/src/core/public/overlays/flyout.tsx index c5b68cb19b2b8..199e975a0b26c 100644 --- a/src/core/public/overlays/flyout.tsx +++ b/src/core/public/overlays/flyout.tsx @@ -24,6 +24,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Subject } from 'rxjs'; import { I18nSetup } from '../i18n'; +import { OverlayRef } from './overlay_service'; /** * A FlyoutRef is a reference to an opened flyout panel. It offers methods to @@ -36,7 +37,7 @@ import { I18nSetup } from '../i18n'; * * @public */ -export class FlyoutRef { +export class FlyoutRef implements OverlayRef { /** * An Promise that will resolve once this flyout is closed. * diff --git a/src/core/public/overlays/index.ts b/src/core/public/overlays/index.ts index 246cc7d4621f1..6e92b0e84f8a0 100644 --- a/src/core/public/overlays/index.ts +++ b/src/core/public/overlays/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export { OverlayService, OverlayStart } from './overlay_service'; -export { FlyoutRef } from './flyout'; +export { OverlayService, OverlayStart, OverlayRef } from './overlay_service'; diff --git a/src/core/public/overlays/modal.test.tsx b/src/core/public/overlays/modal.test.tsx new file mode 100644 index 0000000000000..452acddf7d1f8 --- /dev/null +++ b/src/core/public/overlays/modal.test.tsx @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { mockReactDomRender, mockReactDomUnmount } from './flyout.test.mocks'; + +import React from 'react'; +import { i18nServiceMock } from '../i18n/i18n_service.mock'; +import { ModalService, ModalRef } from './modal'; + +const i18nMock = i18nServiceMock.createSetupContract(); + +beforeEach(() => { + mockReactDomRender.mockClear(); + mockReactDomUnmount.mockClear(); +}); + +describe('ModalService', () => { + describe('openModal()', () => { + it('renders a modal to the DOM', () => { + const target = document.createElement('div'); + const modalService = new ModalService(target); + expect(mockReactDomRender).not.toHaveBeenCalled(); + modalService.openModal(i18nMock, Modal content); + expect(mockReactDomRender.mock.calls).toMatchSnapshot(); + }); + describe('with a currently active modal', () => { + let target: HTMLElement, modalService: ModalService, ref1: ModalRef; + beforeEach(() => { + target = document.createElement('div'); + modalService = new ModalService(target); + ref1 = modalService.openModal(i18nMock, Modal content 1); + }); + it('replaces the current modal with a new one', () => { + modalService.openModal(i18nMock, Flyout content 2); + expect(mockReactDomRender.mock.calls).toMatchSnapshot(); + expect(mockReactDomUnmount).toHaveBeenCalledTimes(1); + expect(() => ref1.close()).not.toThrowError(); + expect(mockReactDomUnmount).toHaveBeenCalledTimes(1); + }); + it('resolves onClose on the previous ref', async () => { + const onCloseComplete = jest.fn(); + ref1.onClose.then(onCloseComplete); + modalService.openModal(i18nMock, Flyout content 2); + await ref1.onClose; + expect(onCloseComplete).toBeCalledTimes(1); + }); + }); + }); + describe('ModalRef#close()', () => { + it('resolves the onClose Promise', async () => { + const target = document.createElement('div'); + const modalService = new ModalService(target); + const ref = modalService.openModal(i18nMock, Flyout content); + + const onCloseComplete = jest.fn(); + ref.onClose.then(onCloseComplete); + await ref.close(); + await ref.close(); + expect(onCloseComplete).toHaveBeenCalledTimes(1); + }); + it('can be called multiple times on the same ModalRef', async () => { + const target = document.createElement('div'); + const modalService = new ModalService(target); + const ref = modalService.openModal(i18nMock, Flyout content); + expect(mockReactDomUnmount).not.toHaveBeenCalled(); + await ref.close(); + expect(mockReactDomUnmount.mock.calls).toMatchSnapshot(); + await ref.close(); + expect(mockReactDomUnmount).toHaveBeenCalledTimes(1); + }); + it("on a stale ModalRef doesn't affect the active flyout", async () => { + const target = document.createElement('div'); + const modalService = new ModalService(target); + const ref1 = modalService.openModal(i18nMock, Modal content 1); + const ref2 = modalService.openModal(i18nMock, Modal content 2); + const onCloseComplete = jest.fn(); + ref2.onClose.then(onCloseComplete); + mockReactDomUnmount.mockClear(); + await ref1.close(); + expect(mockReactDomUnmount).toBeCalledTimes(0); + expect(onCloseComplete).toBeCalledTimes(0); + }); + }); +}); diff --git a/src/core/public/overlays/modal.tsx b/src/core/public/overlays/modal.tsx new file mode 100644 index 0000000000000..1b1a034b8168b --- /dev/null +++ b/src/core/public/overlays/modal.tsx @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable max-classes-per-file */ + +import { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { Subject } from 'rxjs'; +import { I18nSetup } from '../i18n'; +import { OverlayRef } from './overlay_service'; + +/** + * A ModalRef is a reference to an opened modal. It offers methods to + * close the modal. + * + * @public + */ +export class ModalRef implements OverlayRef { + public readonly onClose: Promise; + + private closeSubject = new Subject(); + + constructor() { + this.onClose = this.closeSubject.toPromise(); + } + + /** + * Closes the referenced modal if it's still open which in turn will + * resolve the `onClose` Promise. If the modal had already been + * closed this method does nothing. + */ + public close(): Promise { + if (!this.closeSubject.closed) { + this.closeSubject.next(); + this.closeSubject.complete(); + } + return this.onClose; + } +} + +/** @internal */ +export class ModalService { + private activeModal: ModalRef | null = null; + + constructor(private readonly targetDomElement: Element) {} + + /** + * Opens a flyout panel with the given component inside. You can use + * `close()` on the returned FlyoutRef to close the flyout. + * + * @param flyoutChildren - Mounts the children inside a flyout panel + * @return {FlyoutRef} A reference to the opened flyout panel. + */ + public openModal = ( + i18n: I18nSetup, + modalChildren: React.ReactNode, + modalProps: { + closeButtonAriaLabel?: string; + 'data-test-subj'?: string; + } = {} + ): ModalRef => { + // If there is an active flyout session close it before opening a new one. + if (this.activeModal) { + this.activeModal.close(); + this.cleanupDom(); + } + + const modal = new ModalRef(); + + // If a modal gets closed through it's ModalRef, remove it from the dom + modal.onClose.then(() => { + if (this.activeModal === modal) { + this.cleanupDom(); + } + }); + + this.activeModal = modal; + + render( + + + modal.close()}> + {modalChildren} + + + , + this.targetDomElement + ); + + return modal; + }; + + /** + * Using React.Render to re-render into a target DOM element will replace + * the content of the target but won't call unmountComponent on any + * components inside the target or any of their children. So we properly + * cleanup the DOM here to prevent subtle bugs in child components which + * depend on unmounting for cleanup behaviour. + */ + private cleanupDom(): void { + unmountComponentAtNode(this.targetDomElement); + this.targetDomElement.innerHTML = ''; + this.activeModal = null; + } +} diff --git a/src/core/public/overlays/overlay_service.mock.ts b/src/core/public/overlays/overlay_service.mock.ts index 186e222326c7b..c6989800cbf78 100644 --- a/src/core/public/overlays/overlay_service.mock.ts +++ b/src/core/public/overlays/overlay_service.mock.ts @@ -21,6 +21,7 @@ import { OverlayService, OverlayStart } from './overlay_service'; const createStartContractMock = () => { const startContract: jest.Mocked> = { openFlyout: jest.fn(), + openModal: jest.fn(), }; return startContract; }; diff --git a/src/core/public/overlays/overlay_service.ts b/src/core/public/overlays/overlay_service.ts index dad96d1dc4f49..961e45f365676 100644 --- a/src/core/public/overlays/overlay_service.ts +++ b/src/core/public/overlays/overlay_service.ts @@ -18,25 +18,47 @@ */ import { FlyoutService } from './flyout'; - -import { FlyoutRef } from '..'; +import { ModalService } from './modal'; import { I18nStart } from '../i18n'; +export interface OverlayRef { + /** + * A Promise that will resolve once this overlay is closed. + * + * Overlays can close from user interaction, calling `close()` on the overlay + * reference or another overlay replacing yours via `openModal` or `openFlyout`. + */ + onClose: Promise; + + /** + * Closes the referenced overlay if it's still open which in turn will + * resolve the `onClose` Promise. If the overlay had already been + * closed this method does nothing. + */ + close(): Promise; +} + interface StartDeps { i18n: I18nStart; + targetDomElement: HTMLElement; } /** @internal */ export class OverlayService { - private flyoutService: FlyoutService; + private flyoutService?: FlyoutService; + private modalService?: ModalService; - constructor(targetDomElement: HTMLElement) { - this.flyoutService = new FlyoutService(targetDomElement); - } + public start({ i18n, targetDomElement }: StartDeps): OverlayStart { + const flyoutElement = document.createElement('div'); + const modalElement = document.createElement('div'); + targetDomElement.appendChild(flyoutElement); + targetDomElement.appendChild(modalElement); + this.flyoutService = new FlyoutService(flyoutElement); + this.modalService = new ModalService(modalElement); - public start({ i18n }: StartDeps): OverlayStart { return { openFlyout: this.flyoutService.openFlyout.bind(this.flyoutService, i18n), + openModal: this.modalService.openModal.bind(this.modalService, i18n), }; } } @@ -49,5 +71,12 @@ export interface OverlayStart { closeButtonAriaLabel?: string; 'data-test-subj'?: string; } - ) => FlyoutRef; + ) => OverlayRef; + openModal: ( + modalChildren: React.ReactNode, + modalProps?: { + closeButtonAriaLabel?: string; + 'data-test-subj'?: string; + } + ) => OverlayRef; } diff --git a/src/core/public/plugins/index.ts b/src/core/public/plugins/index.ts index d6a5d4bf92f4c..3e1e1434c4259 100644 --- a/src/core/public/plugins/index.ts +++ b/src/core/public/plugins/index.ts @@ -19,4 +19,4 @@ export * from './plugins_service'; export { Plugin, PluginInitializer } from './plugin'; -export { PluginInitializerContext, PluginSetupContext } from './plugin_context'; +export { PluginInitializerContext, PluginSetupContext, PluginStartContext } from './plugin_context'; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index e5d3989d4a277..aae002a082bfc 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -29,7 +29,7 @@ import { PluginWrapper } from './plugin'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; import { OverlayStart } from '../overlays'; import { ApplicationStart } from '../application'; -import { HttpSetup } from '../http'; +import { HttpSetup, HttpStart } from '../http'; /** * The available core services passed to a `PluginInitializer` @@ -62,6 +62,7 @@ export interface PluginSetupContext { export interface PluginStartContext { application: Pick; basePath: BasePathStart; + http: HttpStart; i18n: I18nStart; notifications: NotificationsStart; overlays: OverlayStart; @@ -128,6 +129,7 @@ export function createPluginStartContext { mockStartDeps = { application: applicationServiceMock.createStartContract(), basePath: basePathServiceMock.createStartContract(), + http: httpServiceMock.createStartContract(), i18n: i18nServiceMock.createStartContract(), injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 88231460d2533..ed07c6736dfd8 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -37,6 +37,7 @@ export type PluginsServiceStartDeps = CoreStart; export interface PluginsServiceSetup { contracts: Map; } +/** @internal */ export interface PluginsServiceStart { contracts: Map; } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index a03bb50c8e2a3..da2d365fc5932 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -118,15 +118,15 @@ export interface CoreSetup { uiSettings: UiSettingsSetup; } -// Warning: (ae-missing-release-tag) "CoreStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface CoreStart { // (undocumented) application: ApplicationStart; // (undocumented) basePath: BasePathStart; // (undocumented) + http: HttpStart; + // (undocumented) i18n: I18nStart; // (undocumented) injectedMetadata: InjectedMetadataStart; @@ -142,7 +142,7 @@ export class CoreSystem { constructor(params: Params); // (undocumented) setup(): Promise<{ - fatalErrors: import(".").FatalErrorsSetup; + fatalErrors: FatalErrorsSetup; } | undefined>; // (undocumented) start(): Promise; @@ -151,17 +151,17 @@ export class CoreSystem { } // @public -export interface FatalErrorsSetup { - add: (error: string | Error, source?: string) => never; - // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts - get$: () => Rx.Observable; +export interface FatalErrorInfo { + // (undocumented) + message: string; + // (undocumented) + stack: string | undefined; } // @public -export class FlyoutRef { - constructor(); - close(): Promise; - readonly onClose: Promise; +export interface FatalErrorsSetup { + add: (error: string | Error, source?: string) => never; + get$: () => Rx.Observable; } // Warning: (ae-forgotten-export) The symbol "HttpService" needs to be exported by the entry point index.d.ts @@ -169,6 +169,9 @@ export class FlyoutRef { // @public (undocumented) export type HttpSetup = ReturnType; +// @public (undocumented) +export type HttpStart = ReturnType; + // @public export interface I18nSetup { Context: ({ children }: { @@ -176,8 +179,6 @@ export interface I18nSetup { }) => JSX.Element; } -// Warning: (ae-missing-release-tag) "I18nStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type I18nStart = I18nSetup; @@ -233,6 +234,8 @@ export interface InjectedMetadataSetup { [key: string]: unknown; }; // (undocumented) + getKibanaBuildNumber: () => number; + // (undocumented) getKibanaVersion: () => string; // (undocumented) getLegacyMetadata: () => { @@ -286,6 +289,14 @@ export interface NotificationsSetup { // @public (undocumented) export type NotificationsStart = NotificationsSetup; +// Warning: (ae-missing-release-tag) "OverlayRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface OverlayRef { + close(): Promise; + onClose: Promise; +} + // @public (undocumented) export interface OverlayStart { // Warning: (ae-forgotten-export) The symbol "React" needs to be exported by the entry point index.d.ts @@ -294,15 +305,18 @@ export interface OverlayStart { openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: { closeButtonAriaLabel?: string; 'data-test-subj'?: string; - }) => FlyoutRef; + }) => OverlayRef; + // (undocumented) + openModal: (modalChildren: React.ReactNode, modalProps?: { + closeButtonAriaLabel?: string; + 'data-test-subj'?: string; + }) => OverlayRef; } // @public export interface Plugin = {}, TPluginsStart extends Record = {}> { // (undocumented) setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise; - // Warning: (ae-forgotten-export) The symbol "PluginStartContext" needs to be exported by the entry point index.d.ts - // // (undocumented) start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise; // (undocumented) @@ -334,6 +348,22 @@ export interface PluginSetupContext { uiSettings: UiSettingsSetup; } +// @public +export interface PluginStartContext { + // (undocumented) + application: Pick; + // (undocumented) + basePath: BasePathStart; + // (undocumented) + http: HttpStart; + // (undocumented) + i18n: I18nStart; + // (undocumented) + notifications: NotificationsStart; + // (undocumented) + overlays: OverlayStart; +} + export { Toast } // @public (undocumented) @@ -357,6 +387,7 @@ export class ToastsApi { // @public (undocumented) export class UiSettingsClient { + // Warning: (ae-forgotten-export) The symbol "UiSettingsClientParams" needs to be exported by the entry point index.d.ts constructor(params: UiSettingsClientParams); get$(key: string, defaultOverride?: any): Rx.Observable; get(key: string, defaultOverride?: any): any; @@ -377,10 +408,6 @@ export class UiSettingsClient { isDefault(key: string): boolean; isOverridden(key: string): boolean; overrideLocalDefault(key: string, newDefault: any): void; - // Warning: (ae-forgotten-export) The symbol "UiSettingsClientParams" needs to be exported by the entry point index.d.ts - // - // (undocumented) - readonly params: UiSettingsClientParams; remove(key: string): Promise; set(key: string, val: any): Promise; stop(): void; diff --git a/src/core/public/ui_settings/ui_settings_client.ts b/src/core/public/ui_settings/ui_settings_client.ts index fd28c250b8678..9207109d1ce0b 100644 --- a/src/core/public/ui_settings/ui_settings_client.ts +++ b/src/core/public/ui_settings/ui_settings_client.ts @@ -41,7 +41,7 @@ export class UiSettingsClient { private readonly defaults: UiSettingsState; private cache: UiSettingsState; - constructor(readonly params: UiSettingsClientParams) { + constructor(params: UiSettingsClientParams) { this.api = params.api; this.defaults = cloneDeep(params.defaults); this.cache = defaultsDeep({}, this.defaults, cloneDeep(params.initialSettings)); diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index 346f3d4a2274d..b92b7323bba58 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -25,7 +25,7 @@ import { distinctUntilChanged, first, map } from 'rxjs/operators'; import { Config, ConfigPath, ConfigWithSchema, Env } from '.'; import { Logger, LoggerFactory } from '../logging'; -/** @public */ +/** @internal */ export class ConfigService { private readonly log: Logger; diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index 614acbb6b7f72..3c7bce831cdf9 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -17,15 +17,10 @@ * under the License. */ -/** @internal */ export { ConfigService } from './config_service'; -/** @internal */ export { RawConfigService } from './raw_config_service'; -/** @internal */ export { Config, ConfigPath, isConfigPath } from './config'; -/** @internal */ export { ObjectToConfigAdapter } from './object_to_config_adapter'; -/** @internal */ export { CliArgs } from './env'; export { Env, EnvironmentMode, PackageInfo } from './env'; diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 75547074bedb6..21e1193471972 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -30,6 +30,7 @@ import { ByteSizeValue } from '@kbn/config-schema'; import { HttpConfig, Router } from '.'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { HttpServer } from './http_server'; +import { KibanaRequest } from './router'; const chance = new Chance(); @@ -613,3 +614,97 @@ test('throws an error if starts without set up', async () => { `"Http server is not setup up yet"` ); }); + +test('#getBasePathFor() returns base path associated with an incoming request', async () => { + const { + getBasePathFor, + setBasePathFor, + registerRouter, + server: innerServer, + registerOnRequest, + } = await server.setup(config); + + const path = '/base-path'; + registerOnRequest((req, t) => { + setBasePathFor(req, path); + return t.next(); + }); + + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: getBasePathFor(req) })); + registerRouter(router); + + await server.start(config); + await supertest(innerServer.listener) + .get('/') + .expect(200) + .then(res => { + expect(res.body).toEqual({ key: path }); + }); +}); + +test('#getBasePathFor() is based on server base path', async () => { + const configWithBasePath = { + ...config, + basePath: '/bar', + }; + const { + getBasePathFor, + setBasePathFor, + registerRouter, + server: innerServer, + registerOnRequest, + } = await server.setup(configWithBasePath); + + const path = '/base-path'; + registerOnRequest((req, t) => { + setBasePathFor(req, path); + return t.next(); + }); + + const router = new Router('/'); + router.get({ path: '/', validate: false }, async (req, res) => + res.ok({ key: getBasePathFor(req) }) + ); + registerRouter(router); + + await server.start(configWithBasePath); + await supertest(innerServer.listener) + .get('/') + .expect(200) + .then(res => { + expect(res.body).toEqual({ key: `${configWithBasePath.basePath}${path}` }); + }); +}); + +test('#setBasePathFor() cannot be set twice for one request', async () => { + const incomingMessage = { + url: '/', + }; + const kibanaRequestFactory = { + from() { + return KibanaRequest.from( + { + headers: {}, + path: '/', + raw: { + req: incomingMessage, + }, + } as any, + undefined + ); + }, + }; + jest.doMock('./router/request', () => ({ + KibanaRequest: jest.fn(() => kibanaRequestFactory), + })); + + const { setBasePathFor } = await server.setup(config); + + const setPath = () => setBasePathFor(kibanaRequestFactory.from(), '/path'); + + setPath(); + expect(setPath).toThrowErrorMatchingInlineSnapshot( + `"Request basePath was previously set. Setting multiple times is not supported."` + ); +}); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 1e7361c8df535..6dbae8a14d601 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Server, ServerOptions } from 'hapi'; +import { Request, Server, ServerOptions } from 'hapi'; import { modifyUrl } from '../../utils'; import { Logger } from '../logging'; @@ -25,8 +25,7 @@ import { HttpConfig } from './http_config'; import { createServer, getServerOptions } from './http_tools'; import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnRequestFormat, OnRequestHandler } from './lifecycle/on_request'; -import { Router } from './router'; - +import { Router, KibanaRequest } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -50,12 +49,18 @@ export interface HttpServerSetup { * Can register any number of OnRequestHandlers, which are called in sequence (from the first registered to the last) */ registerOnRequest: (requestHandler: OnRequestHandler) => void; + getBasePathFor: (request: KibanaRequest | Request) => string; + setBasePathFor: (request: KibanaRequest | Request, basePath: string) => void; } export class HttpServer { private server?: Server; private registeredRouters = new Set(); private authRegistered = false; + private basePathCache = new WeakMap< + ReturnType, + string + >(); constructor(private readonly log: Logger) {} @@ -72,6 +77,28 @@ export class HttpServer { this.registeredRouters.add(router); } + // passing hapi Request works for BWC. can be deleted once we remove legacy server. + private getBasePathFor(config: HttpConfig, request: KibanaRequest | Request) { + const incomingMessage = + request instanceof KibanaRequest ? request.unstable_getIncomingMessage() : request.raw.req; + + const requestScopePath = this.basePathCache.get(incomingMessage) || ''; + const serverBasePath = config.basePath || ''; + return `${serverBasePath}${requestScopePath}`; + } + + // should work only for KibanaRequest as soon as spaces migrate to NP + private setBasePathFor(request: KibanaRequest | Request, basePath: string) { + const incomingMessage = + request instanceof KibanaRequest ? request.unstable_getIncomingMessage() : request.raw.req; + if (this.basePathCache.has(incomingMessage)) { + throw new Error( + 'Request basePath was previously set. Setting multiple times is not supported.' + ); + } + this.basePathCache.set(incomingMessage, basePath); + } + public setup(config: HttpConfig): HttpServerSetup { const serverOptions = getServerOptions(config); this.server = createServer(serverOptions); @@ -84,6 +111,8 @@ export class HttpServer { fn: AuthenticationHandler, cookieOptions: SessionStorageCookieOptions ) => this.registerAuth(fn, cookieOptions, config.basePath), + getBasePathFor: this.getBasePathFor.bind(this, config), + setBasePathFor: this.setBasePathFor.bind(this), // Return server instance with the connection options so that we can properly // bridge core and the "legacy" Kibana internally. Once this bridge isn't // needed anymore we shouldn't return the instance from this method. diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index fe9b8a082ace5..289eae0990531 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -26,6 +26,8 @@ const createSetupContractMock = () => { registerAuth: jest.fn(), registerOnRequest: jest.fn(), registerRouter: jest.fn(), + getBasePathFor: jest.fn(), + setBasePathFor: jest.fn(), // we can mock some hapi server method when we need it server: {} as Server, }; diff --git a/src/core/server/http/integration_tests/http_service.test.ts b/src/core/server/http/integration_tests/http_service.test.ts index ac2e5ebbe5cbe..c635429159fe3 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -18,6 +18,8 @@ */ import path from 'path'; +import { parse } from 'url'; + import request from 'request'; import * as kbnTestServer from '../../../../test_utils/kbn_server'; import { Router } from '../router'; @@ -27,12 +29,12 @@ import { url as onReqUrl } from './__fixtures__/plugins/dummy_on_request/server/ describe('http service', () => { describe('setup contract', () => { describe('#registerAuth()', () => { - const dummySecurityPlugin = path.resolve(__dirname, './__fixtures__/plugins/dummy_security'); + const plugin = path.resolve(__dirname, './__fixtures__/plugins/dummy_security'); let root: ReturnType; beforeAll(async () => { root = kbnTestServer.createRoot( { - plugins: { paths: [dummySecurityPlugin] }, + plugins: { paths: [plugin] }, }, { dev: true, @@ -109,15 +111,12 @@ describe('http service', () => { }); describe('#registerOnRequest()', () => { - const dummyOnRequestPlugin = path.resolve( - __dirname, - './__fixtures__/plugins/dummy_on_request' - ); + const plugin = path.resolve(__dirname, './__fixtures__/plugins/dummy_on_request'); let root: ReturnType; - beforeAll(async () => { + beforeEach(async () => { root = kbnTestServer.createRoot( { - plugins: { paths: [dummyOnRequestPlugin] }, + plugins: { paths: [plugin] }, }, { dev: true, @@ -136,7 +135,7 @@ describe('http service', () => { await root.start(); }, 30000); - afterAll(async () => await root.shutdown()); + afterEach(async () => await root.shutdown()); it('Should support passing request through to the route handler', async () => { await kbnTestServer.request.get(root, onReqUrl.root).expect(200, { content: 'ok' }); }); @@ -160,5 +159,79 @@ describe('http service', () => { await kbnTestServer.request.get(root, onReqUrl.independentReq).expect(200); }); }); + + describe('#registerOnRequest() toolkit', () => { + let root: ReturnType; + beforeEach(async () => { + root = kbnTestServer.createRoot(); + }, 30000); + + afterEach(async () => await root.shutdown()); + it('supports Url change on the flight', async () => { + const { http } = await root.setup(); + http.registerOnRequest((req, t) => { + t.setUrl(parse('/new-url')); + return t.next(); + }); + + const router = new Router('/'); + router.get({ path: '/new-url', validate: false }, async (req, res) => + res.ok({ key: 'new-url-reached' }) + ); + http.registerRouter(router); + + await root.start(); + + await kbnTestServer.request.get(root, '/').expect(200, { key: 'new-url-reached' }); + }); + + it('url re-write works for legacy server as well', async () => { + const { http } = await root.setup(); + const newUrl = '/new-url'; + http.registerOnRequest((req, t) => { + t.setUrl(newUrl); + return t.next(); + }); + + await root.start(); + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: newUrl, + handler: () => 'ok-from-legacy', + }); + + await kbnTestServer.request.get(root, '/').expect(200, 'ok-from-legacy'); + }); + }); + + describe('#getBasePathFor()/#setBasePathFor()', () => { + let root: ReturnType; + beforeEach(async () => { + root = kbnTestServer.createRoot(); + }, 30000); + + afterEach(async () => await root.shutdown()); + it('basePath information for an incoming request is available in legacy server', async () => { + const reqBasePath = '/requests-specific-base-path'; + const { http } = await root.setup(); + http.registerOnRequest((req, t) => { + http.setBasePathFor(req, reqBasePath); + return t.next(); + }); + + await root.start(); + + const legacyUrl = '/legacy'; + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: legacyUrl, + handler: kbnServer.newPlatform.setup.core.http.getBasePathFor, + }); + + await kbnTestServer.request.get(root, legacyUrl).expect(200, reqBasePath); + }); + }); }); }); diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts index 6192a1dae682c..168b4f513400f 100644 --- a/src/core/server/http/lifecycle/on_request.ts +++ b/src/core/server/http/lifecycle/on_request.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Url } from 'url'; import Boom from 'boom'; import { Lifecycle, Request, ResponseToolkit } from 'hapi'; import { KibanaRequest } from '../router'; @@ -64,14 +65,10 @@ export interface OnRequestToolkit { redirected: (url: string) => OnRequestResult; /** Fail the request with specified error. */ rejected: (error: Error, options?: { statusCode?: number }) => OnRequestResult; + /** Change url for an incoming request. */ + setUrl: (newUrl: string | Url) => void; } -const toolkit: OnRequestToolkit = { - next: OnRequestResult.next, - redirected: OnRequestResult.redirected, - rejected: OnRequestResult.rejected, -}; - /** @public */ export type OnRequestHandler = ( req: KibanaRequest, @@ -86,11 +83,20 @@ export type OnRequestHandler = ( */ export function adoptToHapiOnRequestFormat(fn: OnRequestHandler) { return async function interceptRequest( - req: Request, + request: Request, h: ResponseToolkit ): Promise { try { - const result = await fn(KibanaRequest.from(req, undefined), toolkit); + const result = await fn(KibanaRequest.from(request, undefined), { + next: OnRequestResult.next, + redirected: OnRequestResult.redirected, + rejected: OnRequestResult.rejected, + setUrl: (newUrl: string | Url) => { + request.setUrl(newUrl); + // We should update raw request as well since it can be proxied to the old platform + request.raw.req.url = typeof newUrl === 'string' ? newUrl : newUrl.href; + }, + }); if (OnRequestResult.isValidResult(result)) { if (result.isNext()) { return h.continue; diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 69de94e5fc6da..03b62f4948306 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -24,7 +24,7 @@ import { filterHeaders, Headers } from './headers'; import { RouteSchemas } from './route'; /** @public */ -export class KibanaRequest { +export class KibanaRequest { /** * Factory for creating requests. Validates the request before creating an * instance of a KibanaRequest. @@ -71,12 +71,22 @@ export class KibanaRequest { public readonly headers: Headers; public readonly path: string; - constructor(req: Request, readonly params: Params, readonly query: Query, readonly body: Body) { - this.headers = req.headers; - this.path = req.path; + constructor( + private readonly request: Request, + readonly params: Params, + readonly query: Query, + readonly body: Body + ) { + this.headers = request.headers; + this.path = request.path; } public getFilteredHeaders(headersToKeep: string[]) { return filterHeaders(this.headers, headersToKeep); } + + // eslint-disable-next-line @typescript-eslint/camelcase + public unstable_getIncomingMessage() { + return this.request.raw.req; + } } diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index e75045007e4fd..a640a413fd81b 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -171,4 +171,4 @@ export class Router { export type RequestHandler

= ( req: KibanaRequest, TypeOf, TypeOf>, createResponse: ResponseFactory -) => Promise>; +) => KibanaResponse | Promise>; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 448433b5420a9..8d8b38d2e78de 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -57,6 +57,9 @@ export interface CoreSetup { plugins: PluginsServiceSetup; } +/** + * @public + */ export interface CoreStart { http: HttpServiceStart; plugins: PluginsServiceStart; diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 72981005968f3..bedb374536970 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -85,7 +85,7 @@ export interface PluginManifest { /** * Small container object used to expose information about discovered plugins that may * or may not have been started. - * @internal + * @public */ export interface DiscoveredPlugin { /** diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index b738e192896dd..f216ae6d6fb5e 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -58,6 +58,8 @@ export interface PluginSetupContext { http: { registerAuth: HttpServiceSetup['registerAuth']; registerOnRequest: HttpServiceSetup['registerOnRequest']; + getBasePathFor: HttpServiceSetup['getBasePathFor']; + setBasePathFor: HttpServiceSetup['setBasePathFor']; }; } @@ -148,6 +150,8 @@ export function createPluginSetupContext( http: { registerAuth: deps.http.registerAuth, registerOnRequest: deps.http.registerOnRequest, + getBasePathFor: deps.http.getBasePathFor, + setBasePathFor: deps.http.setBasePathFor, }, }; } diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index f243754db8415..98fa08931e23d 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -29,7 +29,7 @@ import { DiscoveredPlugin, DiscoveredPluginInternal, PluginWrapper, PluginName } import { PluginsConfig } from './plugins_config'; import { PluginsSystem } from './plugins_system'; -/** @internal */ +/** @public */ export interface PluginsServiceSetup { contracts: Map; uiPlugins: { @@ -38,7 +38,7 @@ export interface PluginsServiceSetup { }; } -/** @internal */ +/** @public */ export interface PluginsServiceStart { contracts: Map; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 0d51fce8f88f5..d4ba9e3bb8208 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -16,6 +16,7 @@ import { Server } from 'hapi'; import { ServerOptions } from 'hapi'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; +import { Url } from 'url'; // @public (undocumented) export type APICaller = (endpoint: string, clientParams: Record, options?: CallAPIOptions) => Promise; @@ -55,7 +56,7 @@ export class ClusterClient { close(): void; } -// @public (undocumented) +// @internal (undocumented) export class ConfigService { // Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Env" needs to be exported by the entry point index.d.ts @@ -79,25 +80,19 @@ export interface CoreSetup { elasticsearch: ElasticsearchServiceSetup; // (undocumented) http: HttpServiceSetup; - // Warning: (ae-incompatible-release-tags) The symbol "plugins" is marked as @public, but its signature references "PluginsServiceSetup" which is marked as @internal - // // (undocumented) plugins: PluginsServiceSetup; } -// Warning: (ae-missing-release-tag) "CoreStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export interface CoreStart { // (undocumented) http: HttpServiceStart; - // Warning: (ae-incompatible-release-tags) The symbol "plugins" is marked as @public, but its signature references "PluginsServiceStart" which is marked as @internal - // // (undocumented) plugins: PluginsServiceStart; } -// @internal +// @public export interface DiscoveredPlugin { readonly configPath: ConfigPath; readonly id: PluginName; @@ -143,8 +138,8 @@ export interface HttpServiceStart { } // @public (undocumented) -export class KibanaRequest { - constructor(req: Request, params: Params, query: Query, body: Body); +export class KibanaRequest { + constructor(request: Request, params: Params, query: Query, body: Body); // (undocumented) readonly body: Body; // Warning: (ae-forgotten-export) The symbol "RouteSchemas" needs to be exported by the entry point index.d.ts @@ -159,6 +154,8 @@ export class KibanaRequest { readonly path: string; // (undocumented) readonly query: Query; + // (undocumented) + unstable_getIncomingMessage(): import("http").IncomingMessage; } // @public @@ -242,6 +239,7 @@ export interface OnRequestToolkit { rejected: (error: Error, options?: { statusCode?: number; }) => OnRequestResult; + setUrl: (newUrl: string | Url) => void; } // @public @@ -286,10 +284,12 @@ export interface PluginSetupContext { http: { registerAuth: HttpServiceSetup['registerAuth']; registerOnRequest: HttpServiceSetup['registerOnRequest']; + getBasePathFor: HttpServiceSetup['getBasePathFor']; + setBasePathFor: HttpServiceSetup['setBasePathFor']; }; } -// @internal (undocumented) +// @public (undocumented) export interface PluginsServiceSetup { // (undocumented) contracts: Map; @@ -300,7 +300,7 @@ export interface PluginsServiceSetup { }; } -// @internal (undocumented) +// @public (undocumented) export interface PluginsServiceStart { // (undocumented) contracts: Map; diff --git a/src/dev/build/lib/runner.js b/src/dev/build/lib/runner.js index b4ef4921ba397..363cfbe97afad 100644 --- a/src/dev/build/lib/runner.js +++ b/src/dev/build/lib/runner.js @@ -24,7 +24,7 @@ import { isErrorLogged, markErrorLogged } from './errors'; import { createBuild } from './build'; export function createRunner({ config, log, buildOssDist, buildDefaultDist }) { - async function execTask(desc, fn, ...args) { + async function execTask(desc, task, ...args) { log.info(desc); log.indent(4); @@ -37,7 +37,7 @@ export function createRunner({ config, log, buildOssDist, buildDefaultDist }) { }; try { - await fn(config, log, ...args); + await task.run(config, log, ...args); log.success(chalk.green('✓'), time()); } catch (error) { if (!isErrorLogged(error)) { @@ -82,10 +82,10 @@ export function createRunner({ config, log, buildOssDist, buildDefaultDist }) { */ return async function run(task) { if (task.global) { - await execTask(chalk`{dim [ global ]} ${task.description}`, task.run, builds); + await execTask(chalk`{dim [ global ]} ${task.description}`, task, builds); } else { for (const build of builds) { - await execTask(`${build.getLogTag()} ${task.description}`, task.run, build); + await execTask(`${build.getLogTag()} ${task.description}`, task, build); } } }; diff --git a/src/dev/build/tasks/create_package_json_task.js b/src/dev/build/tasks/create_package_json_task.js index d3f45e2c05f9a..9a5334c1d5fa5 100644 --- a/src/dev/build/tasks/create_package_json_task.js +++ b/src/dev/build/tasks/create_package_json_task.js @@ -44,8 +44,8 @@ export const CreatePackageJsonTask = { engines: { node: pkg.engines.node, }, - workspaces: pkg.workspaces, resolutions: pkg.resolutions, + workspaces: pkg.workspaces, dependencies: pkg.dependencies }; @@ -69,6 +69,7 @@ export const RemovePackageJsonDepsTask = { delete pkg.dependencies; delete pkg.private; + delete pkg.resolutions; await write( build.resolvePath('package.json'), diff --git a/src/dev/build/tasks/nodejs/extract_node_builds_task.js b/src/dev/build/tasks/nodejs/extract_node_builds_task.js index ce8719911e4ca..f8e767ac79a4b 100644 --- a/src/dev/build/tasks/nodejs/extract_node_builds_task.js +++ b/src/dev/build/tasks/nodejs/extract_node_builds_task.js @@ -19,7 +19,7 @@ import { dirname, resolve } from 'path'; import fs from 'fs'; -import { promisify } from 'bluebird'; +import { promisify } from 'util'; import mkdirp from 'mkdirp'; import { untar } from '../../lib'; @@ -29,7 +29,7 @@ const statAsync = promisify(fs.stat); const mkdirpAsync = promisify(mkdirp); const copyFileAsync = promisify(fs.copyFile); -const ExtractNodeBuildsTask = { +export const ExtractNodeBuildsTask = { global: true, description: 'Extracting node.js builds for all platforms', async run(config) { @@ -55,7 +55,3 @@ const ExtractNodeBuildsTask = { return await copyFileAsync(source, destination, fs.constants.COPYFILE_FICLONE); }, }; - -ExtractNodeBuildsTask.run = ExtractNodeBuildsTask.run.bind(ExtractNodeBuildsTask); - -export { ExtractNodeBuildsTask }; diff --git a/src/dev/eslint/pick_files_to_lint.js b/src/dev/eslint/pick_files_to_lint.js index 2d3ec9a58e70f..e3212c00d9e0d 100644 --- a/src/dev/eslint/pick_files_to_lint.js +++ b/src/dev/eslint/pick_files_to_lint.js @@ -30,7 +30,7 @@ export function pickFilesToLint(log, files) { const cli = new CLIEngine(); return files.filter(file => { - if (!file.isJs()) { + if (!file.isJs() && !file.isTypescript()) { return; } diff --git a/src/dev/license_checker/config.js b/src/dev/license_checker/config.js index 04b0ceedb4168..6c72c918279d7 100644 --- a/src/dev/license_checker/config.js +++ b/src/dev/license_checker/config.js @@ -52,6 +52,7 @@ export const LICENSE_WHITELIST = [ 'CC-BY-3.0', 'CC-BY-4.0', 'Eclipse Distribution License - v 1.0', + 'FreeBSD', 'ISC', 'ISC*', 'MIT OR GPL-2.0', diff --git a/src/dev/precommit_hook/check_file_casing.js b/src/dev/precommit_hook/check_file_casing.js index 943e2428a0922..d4781bb78ecf3 100644 --- a/src/dev/precommit_hook/check_file_casing.js +++ b/src/dev/precommit_hook/check_file_casing.js @@ -119,14 +119,14 @@ async function checkForSnakeCase(log, files) { const ignored = matchesAnyGlob(path, IGNORE_FILE_GLOBS); if (ignored) { - log.debug('%j ignored', file); + log.debug('[casing] %j ignored', file); return; } const pathToValidate = getPathWithoutIgnoredParents(file); const invalid = NON_SNAKE_CASE_RE.test(pathToValidate); if (!invalid) { - log.debug('%j uses valid casing', file); + log.debug('[casing] %j uses valid casing', file); } else { const ignoredParent = file.getRelativePath().slice(0, -pathToValidate.length); errorPaths.push(`${dim(ignoredParent)}${pathToValidate}`); diff --git a/src/es_archiver/actions/load.js b/src/es_archiver/actions/load.js index f3fe468eea72d..2f26ddf87a0ba 100644 --- a/src/es_archiver/actions/load.js +++ b/src/es_archiver/actions/load.js @@ -34,6 +34,7 @@ import { createCreateIndexStream, createIndexDocRecordsStream, migrateKibanaIndex, + Progress, } from '../lib'; // pipe a series of streams into each other so that data and errors @@ -66,12 +67,16 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kib { objectMode: true } ); + const progress = new Progress('load progress'); + progress.activate(log); + await createPromiseFromStreams([ recordStream, createCreateIndexStream({ client, stats, skipExisting, log, kibanaUrl }), - createIndexDocRecordsStream(client, stats), + createIndexDocRecordsStream(client, stats, progress), ]); + progress.deactivate(); const result = stats.toJSON(); for (const [index, { docs }] of Object.entries(result)) { diff --git a/src/es_archiver/actions/save.js b/src/es_archiver/actions/save.js index db32907c36bfd..610e85f3a89ac 100644 --- a/src/es_archiver/actions/save.js +++ b/src/es_archiver/actions/save.js @@ -33,6 +33,7 @@ import { createGenerateIndexRecordsStream, createFormatArchiveStreams, createGenerateDocRecordsStream, + Progress } from '../lib'; export async function saveAction({ name, indices, client, dataDir, log, raw }) { @@ -43,6 +44,9 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { await fromNode(cb => mkdirp(outputDir, cb)); + const progress = new Progress(); + progress.activate(log); + await Promise.all([ // export and save the matching indices to mappings.json createPromiseFromStreams([ @@ -55,12 +59,13 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { // export all documents from matching indexes into data.json.gz createPromiseFromStreams([ createListStream(indices), - createGenerateDocRecordsStream(client, stats), + createGenerateDocRecordsStream(client, stats, progress), ...createFormatArchiveStreams({ gzip: !raw }), createWriteStream(resolve(outputDir, `data.json${raw ? '' : '.gz'}`)) ]) ]); + progress.deactivate(); stats.forEachIndex((index, { docs }) => { log.info('[%s] Archived %d docs from %j', name, docs.archived, index); }); diff --git a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js index b4f221ea0432a..71ca3b0e41d2f 100644 --- a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js @@ -28,6 +28,7 @@ import { } from '../../../../legacy/utils'; import { createGenerateDocRecordsStream } from '../generate_doc_records_stream'; +import { Progress } from '../../progress'; import { createStubStats, createStubClient, @@ -50,10 +51,14 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { } ]); + const progress = new Progress(); await createPromiseFromStreams([ createListStream(['logstash-*']), - createGenerateDocRecordsStream(client, stats) + createGenerateDocRecordsStream(client, stats, progress) ]); + + expect(progress.getTotal()).to.be(0); + expect(progress.getComplete()).to.be(0); }); it('uses a 1 minute scroll timeout', async () => { @@ -73,10 +78,14 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { } ]); + const progress = new Progress(); await createPromiseFromStreams([ createListStream(['logstash-*']), - createGenerateDocRecordsStream(client, stats) + createGenerateDocRecordsStream(client, stats, progress) ]); + + expect(progress.getTotal()).to.be(0); + expect(progress.getComplete()).to.be(0); }); it('consumes index names and scrolls completely before continuing', async () => { @@ -110,12 +119,13 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { } ]); + const progress = new Progress(); const docRecords = await createPromiseFromStreams([ createListStream([ 'index1', 'index2', ]), - createGenerateDocRecordsStream(client, stats), + createGenerateDocRecordsStream(client, stats, progress), createConcatStream([]) ]); @@ -140,5 +150,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { }, ]); sinon.assert.calledTwice(stats.archivedDoc); + expect(progress.getTotal()).to.be(2); + expect(progress.getComplete()).to.be(2); }); }); diff --git a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js index a0ab1430417f1..e48af1f60509b 100644 --- a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js @@ -25,6 +25,7 @@ import { createPromiseFromStreams, } from '../../../../legacy/utils'; +import { Progress } from '../../progress'; import { createIndexDocRecordsStream } from '../index_doc_records_stream'; import { createStubStats, @@ -57,13 +58,16 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { } ]); const stats = createStubStats(); + const progress = new Progress(); await createPromiseFromStreams([ createListStream(records), - createIndexDocRecordsStream(client, stats), + createIndexDocRecordsStream(client, stats, progress), ]); client.assertNoPendingResponses(); + expect(progress.getComplete()).to.be(1); + expect(progress.getTotal()).to.be(undefined); }); it('consumes multiple doc records and sends to `_bulk` api together', async () => { @@ -85,13 +89,16 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { } ]); const stats = createStubStats(); + const progress = new Progress(); await createPromiseFromStreams([ createListStream(records), - createIndexDocRecordsStream(client, stats), + createIndexDocRecordsStream(client, stats, progress), ]); client.assertNoPendingResponses(); + expect(progress.getComplete()).to.be(10); + expect(progress.getTotal()).to.be(undefined); }); it('waits until request is complete before sending more', async () => { @@ -117,13 +124,16 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { return { ok: true }; } ]); + const progress = new Progress(); await createPromiseFromStreams([ createListStream(records), - createIndexDocRecordsStream(client, stats) + createIndexDocRecordsStream(client, stats, progress) ]); client.assertNoPendingResponses(); + expect(progress.getComplete()).to.be(10); + expect(progress.getTotal()).to.be(undefined); }); it('sends a maximum of 300 documents at a time', async () => { @@ -146,13 +156,16 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { return { ok: true }; }, ]); + const progress = new Progress(); await createPromiseFromStreams([ createListStream(records), - createIndexDocRecordsStream(client, stats), + createIndexDocRecordsStream(client, stats, progress), ]); client.assertNoPendingResponses(); + expect(progress.getComplete()).to.be(301); + expect(progress.getTotal()).to.be(undefined); }); it('emits an error if any request fails', async () => { @@ -162,11 +175,12 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { async () => ({ ok: true }), async () => ({ errors: true, forcedError: true }) ]); + const progress = new Progress(); try { await createPromiseFromStreams([ createListStream(records), - createIndexDocRecordsStream(client, stats), + createIndexDocRecordsStream(client, stats, progress), ]); throw new Error('expected stream to emit error'); } catch (err) { @@ -174,5 +188,7 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { } client.assertNoPendingResponses(); + expect(progress.getComplete()).to.be(1); + expect(progress.getTotal()).to.be(undefined); }); }); diff --git a/src/es_archiver/lib/docs/generate_doc_records_stream.js b/src/es_archiver/lib/docs/generate_doc_records_stream.js index d960080ad4198..462020389e489 100644 --- a/src/es_archiver/lib/docs/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/generate_doc_records_stream.js @@ -22,7 +22,7 @@ import { Transform } from 'stream'; const SCROLL_SIZE = 1000; const SCROLL_TIMEOUT = '1m'; -export function createGenerateDocRecordsStream(client, stats) { +export function createGenerateDocRecordsStream(client, stats, progress) { return new Transform({ writableObjectMode: true, readableObjectMode: true, @@ -41,6 +41,7 @@ export function createGenerateDocRecordsStream(client, stats) { rest_total_hits_as_int: true }); remainingHits = resp.hits.total; + progress.addToTotal(remainingHits); } else { resp = await client.scroll({ scrollId: resp._scroll_id, @@ -63,6 +64,8 @@ export function createGenerateDocRecordsStream(client, stats) { } }); } + + progress.addToComplete(resp.hits.hits.length); } callback(null); diff --git a/src/es_archiver/lib/docs/index_doc_records_stream.js b/src/es_archiver/lib/docs/index_doc_records_stream.js index ec14e2cac78ec..943b1e6e2f329 100644 --- a/src/es_archiver/lib/docs/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/index_doc_records_stream.js @@ -19,7 +19,7 @@ import { Writable } from 'stream'; -export function createIndexDocRecordsStream(client, stats) { +export function createIndexDocRecordsStream(client, stats, progress) { async function indexDocs(docs) { const body = []; @@ -51,6 +51,7 @@ export function createIndexDocRecordsStream(client, stats) { async write(record, enc, callback) { try { await indexDocs([record.value]); + progress.addToComplete(1); callback(null); } catch (err) { callback(err); @@ -60,6 +61,7 @@ export function createIndexDocRecordsStream(client, stats) { async writev(chunks, callback) { try { await indexDocs(chunks.map(({ chunk: record }) => record.value)); + progress.addToComplete(chunks.length); callback(null); } catch (err) { callback(err); diff --git a/src/es_archiver/lib/index.js b/src/es_archiver/lib/index.js index 0b4ca00755c8b..e334acdb48a38 100644 --- a/src/es_archiver/lib/index.js +++ b/src/es_archiver/lib/index.js @@ -48,3 +48,7 @@ export { export { readDirectory } from './directory'; + +export { + Progress +} from './progress'; diff --git a/src/es_archiver/lib/progress.ts b/src/es_archiver/lib/progress.ts new file mode 100644 index 0000000000000..de3ab51e9cdb7 --- /dev/null +++ b/src/es_archiver/lib/progress.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ToolingLog } from '@kbn/dev-utils'; + +const SECOND = 1000; + +export class Progress { + private total?: number; + private complete?: number; + private loggingInterval?: NodeJS.Timer; + + getTotal() { + return this.total; + } + + getComplete() { + return this.complete; + } + + getPercent() { + if (this.complete === undefined || this.total === undefined) { + return 0; + } + + return Math.round((this.complete / this.total) * 100); + } + + isActive() { + return !!this.loggingInterval; + } + + activate(log: ToolingLog) { + if (this.loggingInterval) { + throw new Error('Progress is already active'); + } + + // if the action takes longer than 10 seconds, log info about the transfer every 10 seconds + this.loggingInterval = setInterval(() => { + if (this.complete === undefined) { + return; + } + + if (this.total === undefined) { + log.info('progress: %d', this.getComplete()); + return; + } + + log.info('progress: %d/%d (%d%)', this.getComplete(), this.getTotal(), this.getPercent()); + }, 10 * SECOND); + } + + deactivate() { + if (!this.loggingInterval) { + throw new Error('Progress is not active'); + } + + clearInterval(this.loggingInterval); + this.loggingInterval = undefined; + } + + addToTotal(n: number) { + this.total = this.total === undefined ? n : this.total + n; + } + + addToComplete(n: number) { + this.complete = this.complete === undefined ? n : this.complete + n; + } +} diff --git a/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar.test.tsx.snap b/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar.test.tsx.snap index 74b5183ba6478..bc0b15ca88e3e 100644 --- a/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar.test.tsx.snap +++ b/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar.test.tsx.snap @@ -1,252 +1,61 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`QueryBar Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true 1`] = ` +exports[`QueryBar Should render the given query 1`] = ` - -

-
-
-
- - } - aria-activedescendant="" - aria-autocomplete="list" - aria-controls="kbnTypeahead__items" - aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" - autoComplete="off" - autoFocus={false} - compressed={false} - data-test-subj="queryInput" - fullWidth={true} - inputRef={[Function]} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - onKeyDown={[Function]} - onKeyUp={[Function]} - placeholder="Search" - role="textbox" - spellCheck={false} - type="text" - value="response:200" - /> -
-
-
- -
- - - - - - -`; - -exports[`QueryBar Should pass the query language to the language switcher 1`] = ` - - - -
-
-
-
- - } - aria-activedescendant="" - aria-autocomplete="list" - aria-controls="kbnTypeahead__items" - aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" - autoComplete="off" - autoFocus={true} - compressed={false} - data-test-subj="queryInput" - fullWidth={true} - inputRef={[Function]} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - onKeyDown={[Function]} - onKeyUp={[Function]} - placeholder="Search" - role="textbox" - spellCheck={false} - type="text" - value="response:200" - /> -
-
-
- -
-
-
- - -
-`; - -exports[`QueryBar Should render the given query 1`] = ` - - - -
-
-
-
- - } - aria-activedescendant="" - aria-autocomplete="list" - aria-controls="kbnTypeahead__items" - aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" - autoComplete="off" - autoFocus={true} - compressed={false} - data-test-subj="queryInput" - fullWidth={true} - inputRef={[Function]} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - onKeyDown={[Function]} - onKeyUp={[Function]} - placeholder="Search" - role="textbox" - spellCheck={false} - type="text" - value="response:200" - /> -
-
-
- -
-
-
diff --git a/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap b/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap new file mode 100644 index 0000000000000..58d409c227397 --- /dev/null +++ b/src/legacy/core_plugins/data/public/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap @@ -0,0 +1,1052 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`QueryBarInput Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true 1`] = ` + + +
+
+
+
+ + } + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" + autoComplete="off" + autoFocus={false} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" + > + + } + compressed={false} + fullWidth={true} + isLoading={false} + > +
+
+ + + + +
+ + + + + } + className="eui-displayBlock" + closePopover={[Function]} + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} + > + +
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+`; + +exports[`QueryBarInput Should pass the query language to the language switcher 1`] = ` + + +
+
+
+
+ + } + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" + autoComplete="off" + autoFocus={true} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" + > + + } + compressed={false} + fullWidth={true} + isLoading={false} + > +
+
+ + + + +
+ + + + + } + className="eui-displayBlock" + closePopover={[Function]} + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} + > + +
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+`; + +exports[`QueryBarInput Should render the given query 1`] = ` + + +
+
+
+
+ + } + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover" + autoComplete="off" + autoFocus={true} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" + > + + } + compressed={false} + fullWidth={true} + isLoading={false} + > +
+
+ + + + +
+ + + + + } + className="eui-displayBlock" + closePopover={[Function]} + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} + > + +
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+`; diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.tsx b/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.tsx index 7afd6595786d5..04352bd4c6a62 100644 --- a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.tsx @@ -17,19 +17,11 @@ * under the License. */ -import { - mockGetAutocompleteProvider, - mockGetAutocompleteSuggestions, - mockPersistedLog, - mockPersistedLogFactory, -} from './query_bar.test.mocks'; +import { mockPersistedLogFactory } from './query_bar_input.test.mocks'; -import { EuiFieldText } from '@elastic/eui'; import React from 'react'; -import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { QueryBar } from './query_bar'; -import { QueryLanguageSwitcher } from './language_switcher'; -import { QueryBarUI } from './query_bar'; const noop = () => { return; @@ -40,11 +32,6 @@ const kqlQuery = { language: 'kuery', }; -const luceneQuery = { - query: 'response:200', - language: 'lucene', -}; - const createMockWebStorage = () => ({ clear: jest.fn(), getItem: jest.fn(), @@ -98,39 +85,6 @@ describe('QueryBar', () => { expect(component).toMatchSnapshot(); }); - it('Should pass the query language to the language switcher', () => { - const component = shallowWithIntl( - - ); - - expect(component).toMatchSnapshot(); - }); - - it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => { - const component = shallowWithIntl( - - ); - - expect(component).toMatchSnapshot(); - }); - it('Should create a unique PersistedLog based on the appName and query language', () => { shallowWithIntl( { expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery'); }); - - it("On language selection, should store the user's preference in localstorage and reset the query", () => { - const mockStorage = createMockStorage(); - const mockCallback = jest.fn(); - - const component = mountWithIntl( - - ); - - component - .find(QueryLanguageSwitcher) - .props() - .onSelectLanguage('lucene'); - expect(mockStorage.set).toHaveBeenCalledWith('kibana.userQueryLanguage', 'lucene'); - expect(mockCallback).toHaveBeenCalledWith({ - dateRange: { - from: 'now-15m', - to: 'now', - }, - query: { - query: '', - language: 'lucene', - }, - }); - }); - - it('Should call onSubmit with the current query when the user hits enter inside the query bar', () => { - const mockCallback = jest.fn(); - - const component = mountWithIntl( - - ); - - const instance = component.instance() as QueryBarUI; - const input = instance.inputRef; - const inputWrapper = component.find(EuiFieldText).find('input'); - inputWrapper.simulate('change', { target: { value: 'extension:jpg' } }); - inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); - - expect(mockCallback).toHaveBeenCalledTimes(1); - expect(mockCallback).toHaveBeenCalledWith({ - dateRange: { - from: 'now-15m', - to: 'now', - }, - query: { - query: 'extension:jpg', - language: 'kuery', - }, - }); - }); - - it('Should use PersistedLog for recent search suggestions', async () => { - const component = mountWithIntl( - - ); - - const instance = component.instance() as QueryBarUI; - const input = instance.inputRef; - const inputWrapper = component.find(EuiFieldText).find('input'); - inputWrapper.simulate('change', { target: { value: 'extension:jpg' } }); - inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); - - expect(mockPersistedLog.add).toHaveBeenCalledWith('extension:jpg'); - - mockPersistedLog.get.mockClear(); - inputWrapper.simulate('change', { target: { value: 'extensi' } }); - expect(mockPersistedLog.get).toHaveBeenCalledTimes(1); - }); - - it('Should get suggestions from the autocomplete provider for the current language', () => { - mountWithIntl( - - ); - - expect(mockGetAutocompleteProvider).toHaveBeenCalledWith('kuery'); - expect(mockGetAutocompleteSuggestions).toHaveBeenCalled(); - }); }); diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.tsx b/src/legacy/core_plugins/data/public/query_bar/components/query_bar.tsx index 339ef665684df..573124a6302b1 100644 --- a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.tsx +++ b/src/legacy/core_plugins/data/public/query_bar/components/query_bar.tsx @@ -22,22 +22,12 @@ import { IndexPattern } from 'ui/index_patterns'; import classNames from 'classnames'; import _ from 'lodash'; -import { compact, debounce, get, isEqual } from 'lodash'; +import { get, isEqual } from 'lodash'; import React, { Component } from 'react'; -import { kfetch } from 'ui/kfetch'; -import { PersistedLog } from 'ui/persisted_log'; import { Storage } from 'ui/storage'; import { timeHistory } from 'ui/timefilter/time_history'; -import { - EuiButton, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiOutsideClickDetector, - EuiSuperDatePicker, -} from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSuperDatePicker } from '@elastic/eui'; // @ts-ignore import { EuiSuperUpdateButton } from '@elastic/eui'; @@ -45,31 +35,13 @@ import { EuiSuperUpdateButton } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { documentationLinks } from 'ui/documentation_links'; import { Toast, toastNotifications } from 'ui/notify'; -import { - AutocompleteSuggestion, - AutocompleteSuggestionType, - getAutocompleteProvider, -} from 'ui/autocomplete_providers'; import chrome from 'ui/chrome'; +import { PersistedLog } from 'ui/persisted_log'; +import { QueryBarInput } from './query_bar_input'; -import { fromUser, matchPairs, toUser } from '../lib'; -import { QueryLanguageSwitcher } from './language_switcher'; -import { SuggestionsComponent } from './typeahead/suggestions_component'; - -const KEY_CODES = { - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - ENTER: 13, - ESC: 27, - TAB: 9, - HOME: 36, - END: 35, -}; +import { getQueryLog } from '../lib/get_query_log'; const config = chrome.getUiSettingsClient(); -const recentSearchType: AutocompleteSuggestionType = 'recentSearch'; interface Query { query: string; @@ -104,10 +76,6 @@ interface Props { interface State { query: Query; inputIsPristine: boolean; - isSuggestionsVisible: boolean; - index: number | null; - suggestions: AutocompleteSuggestion[]; - suggestionLimit: number; currentProps?: Props; dateRangeFrom: string; dateRangeTo: string; @@ -123,7 +91,7 @@ export class QueryBarUI extends Component { let nextQuery = null; if (nextProps.query.query !== prevState.query.query) { nextQuery = { - query: toUser(nextProps.query.query), + query: nextProps.query.query, language: nextProps.query.language, }; } else if (nextProps.query.language !== prevState.query.language) { @@ -171,31 +139,19 @@ export class QueryBarUI extends Component { */ public state = { query: { - query: toUser(this.props.query.query), + query: this.props.query.query, language: this.props.query.language, }, inputIsPristine: true, - isSuggestionsVisible: false, currentProps: this.props, - index: null, - suggestions: [], - suggestionLimit: 50, dateRangeFrom: _.get(this.props, 'dateRangeFrom', 'now-15m'), dateRangeTo: _.get(this.props, 'dateRangeTo', 'now'), isDateRangeInvalid: false, }; - public updateSuggestions = debounce(async () => { - const suggestions = (await this.getSuggestions()) || []; - if (!this.componentIsUnmounting) { - this.setState({ suggestions }); - } - }, 100); - public inputRef: HTMLInputElement | null = null; - private componentIsUnmounting = false; - private persistedLog: PersistedLog | null = null; + private persistedLog: PersistedLog | undefined; public isDirty = () => { if (!this.props.showDatePicker) { @@ -209,176 +165,20 @@ export class QueryBarUI extends Component { ); }; - public increaseLimit = () => { - this.setState({ - suggestionLimit: this.state.suggestionLimit + 50, - }); - }; - - public incrementIndex = (currentIndex: number) => { - let nextIndex = currentIndex + 1; - if (currentIndex === null || nextIndex >= this.state.suggestions.length) { - nextIndex = 0; - } - this.setState({ index: nextIndex }); - }; - - public decrementIndex = (currentIndex: number) => { - const previousIndex = currentIndex - 1; - if (previousIndex < 0) { - this.setState({ index: this.state.suggestions.length - 1 }); - } else { - this.setState({ index: previousIndex }); - } - }; - - public getSuggestions = async () => { - if (!this.inputRef) { - return; - } - - const { - query: { query, language }, - } = this.state; - const recentSearchSuggestions = this.getRecentSearchSuggestions(query); - - const autocompleteProvider = getAutocompleteProvider(language); - if ( - !autocompleteProvider || - !Array.isArray(this.props.indexPatterns) || - compact(this.props.indexPatterns).length === 0 - ) { - return recentSearchSuggestions; - } - - const indexPatterns = this.props.indexPatterns; - const getAutocompleteSuggestions = autocompleteProvider({ config, indexPatterns }); - - const { selectionStart, selectionEnd } = this.inputRef; - if (selectionStart === null || selectionEnd === null) { - return; - } - - const suggestions: AutocompleteSuggestion[] = await getAutocompleteSuggestions({ - query, - selectionStart, - selectionEnd, - }); - return [...suggestions, ...recentSearchSuggestions]; - }; - - public selectSuggestion = ({ - type, - text, - start, - end, - }: { - type: AutocompleteSuggestionType; - text: string; - start: number; - end: number; - }) => { - if (!this.inputRef) { - return; - } - - const query = this.state.query.query; - const { selectionStart, selectionEnd } = this.inputRef; - if (selectionStart === null || selectionEnd === null) { - return; - } - - const value = query.substr(0, selectionStart) + query.substr(selectionEnd); - - this.setState( - { - query: { - ...this.state.query, - query: value.substr(0, start) + text + value.substr(end), - }, - index: null, - }, - () => { - if (!this.inputRef) { - return; - } - - this.inputRef.setSelectionRange(start + text.length, start + text.length); - - if (type === recentSearchType) { - this.onSubmit(); - } else { - this.updateSuggestions(); - } - } - ); - }; - - public getRecentSearchSuggestions = (query: string) => { - if (!this.persistedLog) { - return []; - } - const recentSearches = this.persistedLog.get(); - const matchingRecentSearches = recentSearches.filter(recentQuery => { - const recentQueryString = typeof recentQuery === 'object' ? toUser(recentQuery) : recentQuery; - return recentQueryString.includes(query); - }); - return matchingRecentSearches.map(recentSearch => { - const text = recentSearch; - const start = 0; - const end = query.length; - return { type: recentSearchType, text, start, end }; - }); - }; - - public onOutsideClick = () => { - if (this.state.isSuggestionsVisible) { - this.setState({ isSuggestionsVisible: false, index: null }); - } - }; - - public onClickInput = (event: React.MouseEvent) => { - if (event.target instanceof HTMLInputElement) { - this.onInputChange(event.target.value); - } - }; - public onClickSubmitButton = (event: React.MouseEvent) => { - this.onSubmit(() => event.preventDefault()); - }; - - public onClickSuggestion = (suggestion: AutocompleteSuggestion) => { - if (!this.inputRef) { - return; + if (this.persistedLog) { + this.persistedLog.add(this.state.query.query); } - this.selectSuggestion(suggestion); - this.inputRef.focus(); - }; - - public onMouseEnterSuggestion = (index: number) => { - this.setState({ index }); + this.onSubmit(() => event.preventDefault()); }; - public onInputChange = (value: string) => { - const hasValue = Boolean(value.trim()); - + public onChange = (query: Query) => { this.setState({ - query: { - query: value, - language: this.state.query.language, - }, + query, inputIsPristine: false, - isSuggestionsVisible: hasValue, - index: null, - suggestionLimit: 50, }); }; - public onChange = (event: React.ChangeEvent) => { - this.updateSuggestions(); - this.onInputChange(event.target.value); - }; - public onTimeChange = ({ start, end, @@ -400,83 +200,6 @@ export class QueryBarUI extends Component { ); }; - public onKeyUp = (event: React.KeyboardEvent) => { - if ([KEY_CODES.LEFT, KEY_CODES.RIGHT, KEY_CODES.HOME, KEY_CODES.END].includes(event.keyCode)) { - this.setState({ isSuggestionsVisible: true }); - if (event.target instanceof HTMLInputElement) { - this.onInputChange(event.target.value); - } - } - }; - - public onKeyDown = (event: React.KeyboardEvent) => { - if (event.target instanceof HTMLInputElement) { - const { isSuggestionsVisible, index } = this.state; - const preventDefault = event.preventDefault.bind(event); - const { target, key, metaKey } = event; - const { value, selectionStart, selectionEnd } = target; - const updateQuery = (query: string, newSelectionStart: number, newSelectionEnd: number) => { - this.setState( - { - query: { - ...this.state.query, - query, - }, - }, - () => { - target.setSelectionRange(newSelectionStart, newSelectionEnd); - } - ); - }; - - switch (event.keyCode) { - case KEY_CODES.DOWN: - event.preventDefault(); - if (isSuggestionsVisible && index !== null) { - this.incrementIndex(index); - } else { - this.setState({ isSuggestionsVisible: true, index: 0 }); - } - break; - case KEY_CODES.UP: - event.preventDefault(); - if (isSuggestionsVisible && index !== null) { - this.decrementIndex(index); - } - break; - case KEY_CODES.ENTER: - event.preventDefault(); - if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) { - this.selectSuggestion(this.state.suggestions[index]); - } else { - this.onSubmit(() => event.preventDefault()); - } - break; - case KEY_CODES.ESC: - event.preventDefault(); - this.setState({ isSuggestionsVisible: false, index: null }); - break; - case KEY_CODES.TAB: - this.setState({ isSuggestionsVisible: false, index: null }); - break; - default: - if (selectionStart !== null && selectionEnd !== null) { - matchPairs({ - value, - selectionStart, - selectionEnd, - key, - metaKey, - updateQuery, - preventDefault, - }); - } - - break; - } - } - }; - public onSubmit = (preventDefault?: () => void) => { if (preventDefault) { preventDefault(); @@ -484,10 +207,6 @@ export class QueryBarUI extends Component { this.handleLuceneSyntaxWarning(); - if (this.persistedLog) { - this.persistedLog.add(this.state.query.query); - } - timeHistory.add({ from: this.state.dateRangeFrom, to: this.state.dateRangeTo, @@ -495,7 +214,7 @@ export class QueryBarUI extends Component { this.props.onSubmit({ query: { - query: fromUser(this.state.query.query), + query: this.state.query.query, language: this.state.query.language, }, dateRange: { @@ -503,146 +222,44 @@ export class QueryBarUI extends Component { to: this.state.dateRangeTo, }, }); - this.setState({ isSuggestionsVisible: false }); }; - public onSelectLanguage = (language: string) => { - // Send telemetry info every time the user opts in or out of kuery - // As a result it is important this function only ever gets called in the - // UI component's change handler. - kfetch({ - pathname: '/api/kibana/kql_opt_in_telemetry', - method: 'POST', - body: JSON.stringify({ opt_in: language === 'kuery' }), - }); - - this.props.store.set('kibana.userQueryLanguage', language); - this.props.onSubmit({ - query: { - query: '', - language, - }, - dateRange: { - from: this.state.dateRangeFrom, - to: this.state.dateRangeTo, - }, + private onInputSubmit = (query: Query) => { + this.setState({ query }, () => { + this.onSubmit(); }); }; public componentDidMount() { - this.persistedLog = new PersistedLog( - `typeahead:${this.props.appName}-${this.state.query.language}`, - { - maxLength: config.get('history:limit'), - filterDuplicates: true, - } - ); - this.updateSuggestions(); + this.persistedLog = getQueryLog(this.props.appName, this.props.query.language); } public componentDidUpdate(prevProps: Props) { if (prevProps.query.language !== this.props.query.language) { - this.persistedLog = new PersistedLog( - `typeahead:${this.props.appName}-${this.state.query.language}`, - { - maxLength: config.get('history:limit'), - filterDuplicates: true, - } - ); - this.updateSuggestions(); + this.persistedLog = getQueryLog(this.props.appName, this.props.query.language); } } - public componentWillUnmount() { - this.updateSuggestions.cancel(); - this.componentIsUnmounting = true; - } - public render() { const classes = classNames('kbnQueryBar', { 'kbnQueryBar--withDatePicker': this.props.showDatePicker, }); return ( - + - - {/* position:relative required on container so the suggestions appear under the query bar*/} -
-
-
-
- { - if (node) { - this.inputRef = node; - } - }} - autoComplete="off" - spellCheck={false} - aria-label={this.props.intl.formatMessage( - { - id: 'data.query.queryBar.searchInputAriaLabel', - defaultMessage: - 'You are on search box of {previouslyTranslatedPageTitle} page. Start typing to search and filter the {pageType}', - }, - { - previouslyTranslatedPageTitle: this.props.screenTitle, - pageType: this.props.appName, - } - )} - type="text" - data-test-subj="queryInput" - aria-autocomplete="list" - aria-controls="kbnTypeahead__items" - aria-activedescendant={ - this.state.isSuggestionsVisible ? 'suggestion-' + this.state.index : '' - } - role="textbox" - prepend={this.props.prepend} - append={ - - } - /> -
-
-
- - -
-
+
{this.renderUpdateButton()}
diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.mocks.ts b/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.mocks.ts similarity index 88% rename from src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.mocks.ts rename to src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.mocks.ts index ac1399addb70c..0f0d5e1591b17 100644 --- a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.mocks.ts @@ -17,6 +17,9 @@ * under the License. */ +import { createKfetch } from 'ui/kfetch/kfetch'; +import { setup } from '../../../../../../test_utils/public/kfetch_test_setup'; + const mockChromeFactory = jest.fn(() => { return { getBasePath: () => `foo`, @@ -47,6 +50,7 @@ export const mockPersistedLogFactory = jest.fn Promise.resolve([])); const mockAutocompleteProvider = jest.fn(() => mockGetAutocompleteSuggestions); export const mockGetAutocompleteProvider = jest.fn(() => mockAutocompleteProvider); +const mockKfetch = jest.fn(() => createKfetch(setup().http)); jest.mock('ui/chrome', () => mockChromeFactory()); jest.mock('ui/kfetch', () => ({ @@ -63,6 +67,10 @@ jest.mock('ui/metadata', () => ({ jest.mock('ui/autocomplete_providers', () => ({ getAutocompleteProvider: mockGetAutocompleteProvider, })); +jest.mock('ui/kfetch', () => ({ + __newPlatformSetup__: jest.fn(), + kfetch: mockKfetch, +})); import _ from 'lodash'; // Using doMock to avoid hoisting so that I can override only the debounce method in lodash diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.tsx b/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.tsx new file mode 100644 index 0000000000000..bcd007d4a601e --- /dev/null +++ b/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.test.tsx @@ -0,0 +1,243 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + mockGetAutocompleteProvider, + mockGetAutocompleteSuggestions, + mockPersistedLog, + mockPersistedLogFactory, +} from './query_bar_input.test.mocks'; + +import { EuiFieldText } from '@elastic/eui'; +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { QueryLanguageSwitcher } from './language_switcher'; +import { QueryBarInput, QueryBarInputUI } from './query_bar_input'; + +const noop = () => { + return; +}; + +const kqlQuery = { + query: 'response:200', + language: 'kuery', +}; + +const luceneQuery = { + query: 'response:200', + language: 'lucene', +}; + +const createMockWebStorage = () => ({ + clear: jest.fn(), + getItem: jest.fn(), + key: jest.fn(), + removeItem: jest.fn(), + setItem: jest.fn(), + length: 0, +}); + +const createMockStorage = () => ({ + store: createMockWebStorage(), + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), +}); + +const mockIndexPattern = { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], +}; + +describe('QueryBarInput', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('Should render the given query', () => { + const component = mountWithIntl( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('Should pass the query language to the language switcher', () => { + const component = mountWithIntl( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => { + const component = mountWithIntl( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('Should create a unique PersistedLog based on the appName and query language', () => { + mountWithIntl( + + ); + + expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery'); + }); + + it("On language selection, should store the user's preference in localstorage and reset the query", () => { + const mockStorage = createMockStorage(); + const mockCallback = jest.fn(); + + const component = mountWithIntl( + + ); + + component + .find(QueryLanguageSwitcher) + .props() + .onSelectLanguage('lucene'); + expect(mockStorage.set).toHaveBeenCalledWith('kibana.userQueryLanguage', 'lucene'); + expect(mockCallback).toHaveBeenCalledWith({ query: '', language: 'lucene' }); + }); + + it('Should call onSubmit when the user hits enter inside the query bar', () => { + const mockCallback = jest.fn(); + + const component = mountWithIntl( + + ); + + const instance = component.instance() as QueryBarInputUI; + const input = instance.inputRef; + const inputWrapper = component.find(EuiFieldText).find('input'); + inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); + + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith({ query: 'response:200', language: 'kuery' }); + }); + + it('Should use PersistedLog for recent search suggestions', async () => { + const component = mountWithIntl( + + ); + + const instance = component.instance() as QueryBarInputUI; + const input = instance.inputRef; + const inputWrapper = component.find(EuiFieldText).find('input'); + inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); + + expect(mockPersistedLog.add).toHaveBeenCalledWith('response:200'); + + mockPersistedLog.get.mockClear(); + inputWrapper.simulate('change', { target: { value: 'extensi' } }); + expect(mockPersistedLog.get).toHaveBeenCalled(); + }); + + it('Should get suggestions from the autocomplete provider for the current language', () => { + mountWithIntl( + + ); + + expect(mockGetAutocompleteProvider).toHaveBeenCalledWith('kuery'); + expect(mockGetAutocompleteSuggestions).toHaveBeenCalled(); + }); +}); diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.tsx b/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.tsx new file mode 100644 index 0000000000000..42bf972889e4d --- /dev/null +++ b/src/legacy/core_plugins/data/public/query_bar/components/query_bar_input.tsx @@ -0,0 +1,476 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Component } from 'react'; +import React from 'react'; + +import { EuiFieldText, EuiOutsideClickDetector } from '@elastic/eui'; + +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { + AutocompleteSuggestion, + AutocompleteSuggestionType, + getAutocompleteProvider, +} from 'ui/autocomplete_providers'; +import { debounce, compact } from 'lodash'; +import { IndexPattern } from 'ui/index_patterns'; +import { PersistedLog } from 'ui/persisted_log'; +import chrome from 'ui/chrome'; +import { kfetch } from 'ui/kfetch'; +import { Storage } from 'ui/storage'; +import { fromUser, matchPairs, toUser } from '../lib'; +import { QueryLanguageSwitcher } from './language_switcher'; +import { SuggestionsComponent } from './typeahead/suggestions_component'; +import { getQueryLog } from '../lib/get_query_log'; + +interface Query { + query: string; + language: string; +} + +interface Props { + indexPatterns: IndexPattern[]; + intl: InjectedIntl; + query: Query; + appName: string; + disableAutoFocus?: boolean; + screenTitle: string; + prepend?: any; + store: Storage; + persistedLog?: PersistedLog; + onChange?: (query: Query) => void; + onSubmit?: (query: Query) => void; +} + +interface State { + isSuggestionsVisible: boolean; + index: number | null; + suggestions: AutocompleteSuggestion[]; + suggestionLimit: number; + selectionStart: number | null; + selectionEnd: number | null; +} + +const KEY_CODES = { + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + ENTER: 13, + ESC: 27, + TAB: 9, + HOME: 36, + END: 35, +}; + +const config = chrome.getUiSettingsClient(); +const recentSearchType: AutocompleteSuggestionType = 'recentSearch'; + +export class QueryBarInputUI extends Component { + public state = { + isSuggestionsVisible: false, + index: null, + suggestions: [], + suggestionLimit: 50, + selectionStart: null, + selectionEnd: null, + }; + + public inputRef: HTMLInputElement | null = null; + + private persistedLog: PersistedLog | undefined; + private componentIsUnmounting = false; + + private getQueryString = () => { + return toUser(this.props.query.query); + }; + + private getSuggestions = async () => { + if (!this.inputRef) { + return; + } + + const { + query: { query, language }, + } = this.props; + const recentSearchSuggestions = this.getRecentSearchSuggestions(query); + + const autocompleteProvider = getAutocompleteProvider(language); + if ( + !autocompleteProvider || + !Array.isArray(this.props.indexPatterns) || + compact(this.props.indexPatterns).length === 0 + ) { + return recentSearchSuggestions; + } + + const indexPatterns = this.props.indexPatterns; + const getAutocompleteSuggestions = autocompleteProvider({ config, indexPatterns }); + + const { selectionStart, selectionEnd } = this.inputRef; + if (selectionStart === null || selectionEnd === null) { + return; + } + + const suggestions: AutocompleteSuggestion[] = await getAutocompleteSuggestions({ + query, + selectionStart, + selectionEnd, + }); + return [...suggestions, ...recentSearchSuggestions]; + }; + + private getRecentSearchSuggestions = (query: string) => { + if (!this.persistedLog) { + return []; + } + const recentSearches = this.persistedLog.get(); + const matchingRecentSearches = recentSearches.filter(recentQuery => { + const recentQueryString = typeof recentQuery === 'object' ? toUser(recentQuery) : recentQuery; + return recentQueryString.includes(query); + }); + return matchingRecentSearches.map(recentSearch => { + const text = toUser(recentSearch); + const start = 0; + const end = query.length; + return { type: recentSearchType, text, start, end }; + }); + }; + + private updateSuggestions = debounce(async () => { + const suggestions = (await this.getSuggestions()) || []; + if (!this.componentIsUnmounting) { + this.setState({ suggestions }); + } + }, 100); + + private onSubmit = (query: Query) => { + if (this.props.onSubmit) { + if (this.persistedLog) { + this.persistedLog.add(query.query); + } + + this.props.onSubmit({ query: fromUser(query.query), language: query.language }); + } + }; + + private onChange = (query: Query) => { + this.updateSuggestions(); + + if (this.props.onChange) { + this.props.onChange({ query: fromUser(query.query), language: query.language }); + } + }; + + private onQueryStringChange = (value: string) => { + const hasValue = Boolean(value.trim()); + + this.setState({ + isSuggestionsVisible: hasValue, + index: null, + suggestionLimit: 50, + }); + + this.onChange({ query: value, language: this.props.query.language }); + }; + + private onInputChange = (event: React.ChangeEvent) => { + this.onQueryStringChange(event.target.value); + }; + + private onClickInput = (event: React.MouseEvent) => { + if (event.target instanceof HTMLInputElement) { + this.onQueryStringChange(event.target.value); + } + }; + + private onKeyUp = (event: React.KeyboardEvent) => { + if ([KEY_CODES.LEFT, KEY_CODES.RIGHT, KEY_CODES.HOME, KEY_CODES.END].includes(event.keyCode)) { + this.setState({ isSuggestionsVisible: true }); + if (event.target instanceof HTMLInputElement) { + this.onQueryStringChange(event.target.value); + } + } + }; + + private onKeyDown = (event: React.KeyboardEvent) => { + if (event.target instanceof HTMLInputElement) { + const { isSuggestionsVisible, index } = this.state; + const preventDefault = event.preventDefault.bind(event); + const { target, key, metaKey } = event; + const { value, selectionStart, selectionEnd } = target; + const updateQuery = (query: string, newSelectionStart: number, newSelectionEnd: number) => { + this.onQueryStringChange(query); + this.setState({ + selectionStart: newSelectionStart, + selectionEnd: newSelectionEnd, + }); + }; + + switch (event.keyCode) { + case KEY_CODES.DOWN: + event.preventDefault(); + if (isSuggestionsVisible && index !== null) { + this.incrementIndex(index); + } else { + this.setState({ isSuggestionsVisible: true, index: 0 }); + } + break; + case KEY_CODES.UP: + event.preventDefault(); + if (isSuggestionsVisible && index !== null) { + this.decrementIndex(index); + } + break; + case KEY_CODES.ENTER: + event.preventDefault(); + if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) { + this.selectSuggestion(this.state.suggestions[index]); + } else { + this.onSubmit(this.props.query); + this.setState({ + isSuggestionsVisible: false, + }); + } + break; + case KEY_CODES.ESC: + event.preventDefault(); + this.setState({ isSuggestionsVisible: false, index: null }); + break; + case KEY_CODES.TAB: + this.setState({ isSuggestionsVisible: false, index: null }); + break; + default: + if (selectionStart !== null && selectionEnd !== null) { + matchPairs({ + value, + selectionStart, + selectionEnd, + key, + metaKey, + updateQuery, + preventDefault, + }); + } + + break; + } + } + }; + + private selectSuggestion = ({ + type, + text, + start, + end, + }: { + type: AutocompleteSuggestionType; + text: string; + start: number; + end: number; + }) => { + if (!this.inputRef) { + return; + } + + const query = this.getQueryString(); + const { selectionStart, selectionEnd } = this.inputRef; + if (selectionStart === null || selectionEnd === null) { + return; + } + + const value = query.substr(0, selectionStart) + query.substr(selectionEnd); + const newQueryString = value.substr(0, start) + text + value.substr(end); + + this.onQueryStringChange(newQueryString); + + if (type === recentSearchType) { + this.setState({ isSuggestionsVisible: false, index: null }); + this.onSubmit({ query: newQueryString, language: this.props.query.language }); + } + }; + + private increaseLimit = () => { + this.setState({ + suggestionLimit: this.state.suggestionLimit + 50, + }); + }; + + private incrementIndex = (currentIndex: number) => { + let nextIndex = currentIndex + 1; + if (currentIndex === null || nextIndex >= this.state.suggestions.length) { + nextIndex = 0; + } + this.setState({ index: nextIndex }); + }; + + private decrementIndex = (currentIndex: number) => { + const previousIndex = currentIndex - 1; + if (previousIndex < 0) { + this.setState({ index: this.state.suggestions.length - 1 }); + } else { + this.setState({ index: previousIndex }); + } + }; + + private onSelectLanguage = (language: string) => { + // Send telemetry info every time the user opts in or out of kuery + // As a result it is important this function only ever gets called in the + // UI component's change handler. + kfetch({ + pathname: '/api/kibana/kql_opt_in_telemetry', + method: 'POST', + body: JSON.stringify({ opt_in: language === 'kuery' }), + }); + + this.props.store.set('kibana.userQueryLanguage', language); + + const newQuery = { query: '', language }; + this.onChange(newQuery); + this.onSubmit(newQuery); + }; + + private onOutsideClick = () => { + if (this.state.isSuggestionsVisible) { + this.setState({ isSuggestionsVisible: false, index: null }); + } + }; + + private onClickSuggestion = (suggestion: AutocompleteSuggestion) => { + if (!this.inputRef) { + return; + } + this.selectSuggestion(suggestion); + this.inputRef.focus(); + }; + + public onMouseEnterSuggestion = (index: number) => { + this.setState({ index }); + }; + + public componentDidMount() { + this.persistedLog = this.props.persistedLog + ? this.props.persistedLog + : getQueryLog(this.props.appName, this.props.query.language); + this.updateSuggestions(); + } + + public componentDidUpdate(prevProps: Props) { + this.persistedLog = this.props.persistedLog + ? this.props.persistedLog + : getQueryLog(this.props.appName, this.props.query.language); + this.updateSuggestions(); + + if (this.state.selectionStart !== null && this.state.selectionEnd !== null) { + if (this.inputRef) { + // For some reason the type guard above does not make the compiler happy + // @ts-ignore + this.inputRef.setSelectionRange(this.state.selectionStart, this.state.selectionEnd); + } + this.setState({ + selectionStart: null, + selectionEnd: null, + }); + } + } + + public componentWillUnmount() { + this.updateSuggestions.cancel(); + this.componentIsUnmounting = true; + } + + public render() { + return ( + +
+
+
+
+ { + if (node) { + this.inputRef = node; + } + }} + autoComplete="off" + spellCheck={false} + aria-label={this.props.intl.formatMessage( + { + id: 'data.query.queryBar.searchInputAriaLabel', + defaultMessage: + 'You are on search box of {previouslyTranslatedPageTitle} page. Start typing to search and filter the {pageType}', + }, + { + previouslyTranslatedPageTitle: this.props.screenTitle, + pageType: this.props.appName, + } + )} + type="text" + data-test-subj="queryInput" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-activedescendant={ + this.state.isSuggestionsVisible ? 'suggestion-' + this.state.index : '' + } + role="textbox" + prepend={this.props.prepend} + append={ + + } + /> +
+
+
+ + +
+
+ ); + } +} + +export const QueryBarInput = injectI18n(QueryBarInputUI); diff --git a/src/legacy/ui/public/kfetch/kfetch_abortable.test.ts b/src/legacy/core_plugins/data/public/query_bar/lib/get_query_log.ts similarity index 57% rename from src/legacy/ui/public/kfetch/kfetch_abortable.test.ts rename to src/legacy/core_plugins/data/public/query_bar/lib/get_query_log.ts index bb1c5ac072524..a3b0fe891ff9f 100644 --- a/src/legacy/ui/public/kfetch/kfetch_abortable.test.ts +++ b/src/legacy/core_plugins/data/public/query_bar/lib/get_query_log.ts @@ -17,23 +17,14 @@ * under the License. */ -jest.mock('../chrome', () => ({ - addBasePath: (path: string) => `http://localhost/myBase/${path}`, -})); +import chrome from 'ui/chrome'; +import { PersistedLog } from 'ui/persisted_log'; -jest.mock('../metadata', () => ({ - metadata: { - version: 'my-version', - }, -})); +const config = chrome.getUiSettingsClient(); -import { kfetchAbortable } from './kfetch_abortable'; - -describe('kfetchAbortable', () => { - it('should return an object with a fetching promise and an abort callback', () => { - const { fetching, abort } = kfetchAbortable({ pathname: 'my/path' }); - expect(typeof fetching.then).toBe('function'); - expect(typeof fetching.catch).toBe('function'); - expect(typeof abort).toBe('function'); +export function getQueryLog(appName: string, language: string) { + return new PersistedLog(`typeahead:${appName}-${language}`, { + maxLength: config.get('history:limit'), + filterDuplicates: true, }); -}); +} diff --git a/src/legacy/core_plugins/kibana/common/tutorials/auditbeat_instructions.js b/src/legacy/core_plugins/kibana/common/tutorials/auditbeat_instructions.js new file mode 100644 index 0000000000000..f1a67b6f800a8 --- /dev/null +++ b/src/legacy/core_plugins/kibana/common/tutorials/auditbeat_instructions.js @@ -0,0 +1,489 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { INSTRUCTION_VARIANT } from './instruction_variant'; +import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; +import { getSpaceIdForBeatsTutorial } from '../lib/get_space_id_for_beats_tutorial'; + +export const createAuditbeatInstructions = context => ({ + INSTALL: { + OSX: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Auditbeat? See the [Getting Started Guide]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf auditbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd auditbeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + DEB: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Auditbeat? See the [Getting Started Guide]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i auditbeat-{config.kibana.version}-amd64.deb', + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.debTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', + }, + }), + }, + RPM: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Auditbeat? See the [Getting Started Guide]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi auditbeat-{config.kibana.version}-x86_64.rpm', + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.rpmTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', + }, + }), + }, + WINDOWS: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.windowsTextPre', { + defaultMessage: 'First time using Auditbeat? See the [Getting Started Guide]({guideLinkUrl}).\n\ + 1. Download the Auditbeat Windows zip file from the [Download]({auditbeatLinkUrl}) page.\n\ + 2. Extract the contents of the zip file into {folderPath}.\n\ + 3. Rename the `{directoryName}` directory to `Auditbeat`.\n\ + 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ +**Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ + 5. From the PowerShell prompt, run the following commands to install Auditbeat as a Windows service.', + values: { + folderPath: '`C:\\Program Files`', + guideLinkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + auditbeatLinkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', + directoryName: 'auditbeat-{config.kibana.version}-windows', + } + }), + commands: [ + 'cd "C:\\Program Files\\Auditbeat"', + '.\\install-service-auditbeat.ps1', + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatInstructions.install.windowsTextPost', { + defaultMessage: 'Modify the settings under {propertyName} in the {auditbeatPath} file to point to your Elasticsearch installation.', + values: { + propertyName: '`output.elasticsearch`', + auditbeatPath: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', + } + }), + } + }, + START: { + OSX: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.start.osxTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.start.osxTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + './auditbeat setup', + './auditbeat -e', + ] + }, + DEB: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.start.debTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.start.debTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + 'sudo auditbeat setup', + 'sudo service auditbeat start', + ] + }, + RPM: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.start.rpmTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + 'sudo auditbeat setup', + 'sudo service auditbeat start', + ], + }, + WINDOWS: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.start.windowsTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + '.\\auditbeat.exe setup', + 'Start-Service auditbeat', + ], + }, + }, + CONFIG: { + OSX: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`auditbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context) + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.osxTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), + }, + DEB: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/auditbeat/auditbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context) + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.debTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), + }, + RPM: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/auditbeat/auditbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context) + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.rpmTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), + }, + WINDOWS: { + title: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context) + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), + } + }, +}); + +export const createAuditbeatCloudInstructions = () => ({ + CONFIG: { + OSX: { + title: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`auditbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.osxTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), + }, + DEB: { + title: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`/etc/auditbeat/auditbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.debTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), + }, + RPM: { + title: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`/etc/auditbeat/auditbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.rpmTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), + }, + WINDOWS: { + title: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.auditbeatCloudInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), + } + } +}); + +export function auditbeatStatusCheck() { + return { + title: i18n.translate('kbn.common.tutorials.auditbeatStatusCheck.title', { + defaultMessage: 'Status', + }), + text: i18n.translate('kbn.common.tutorials.auditbeatStatusCheck.text', { + defaultMessage: 'Check that data is received from Auditbeat', + }), + btnLabel: i18n.translate('kbn.common.tutorials.auditbeatStatusCheck.buttonLabel', { + defaultMessage: 'Check data', + }), + success: i18n.translate('kbn.common.tutorials.auditbeatStatusCheck.successText', { + defaultMessage: 'Data successfully received', + }), + error: i18n.translate('kbn.common.tutorials.auditbeatStatusCheck.errorText', { + defaultMessage: 'No data has been received yet', + }), + esHitsCheck: { + index: 'auditbeat-*', + query: { + bool: { + filter: { + term: { + 'agent.type': 'auditbeat', + }, + }, + }, + }, + }, + }; +} + +export function onPremInstructions(platforms, context) { + const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(context); + + const variants = []; + for (let i = 0; i < platforms.length; i++) { + const platform = platforms[i]; + const instructions = []; + instructions.push(AUDITBEAT_INSTRUCTIONS.INSTALL[platform]); + instructions.push(AUDITBEAT_INSTRUCTIONS.CONFIG[platform]); + instructions.push(AUDITBEAT_INSTRUCTIONS.START[platform]); + variants.push({ + id: INSTRUCTION_VARIANT[platform], + instructions: instructions, + }); + } + return { + instructionSets: [ + { + title: i18n.translate('kbn.common.tutorials.auditbeat.premInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), + instructionVariants: variants, + statusCheck: auditbeatStatusCheck(), + }, + ], + }; +} + +export function onPremCloudInstructions(platforms) { + const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(); + const TRYCLOUD_OPTION1 = createTrycloudOption1(); + const TRYCLOUD_OPTION2 = createTrycloudOption2(); + + const variants = []; + for (let i = 0; i < platforms.length; i++) { + const platform = platforms[i]; + variants.push({ + id: INSTRUCTION_VARIANT[platform], + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + AUDITBEAT_INSTRUCTIONS.INSTALL[platform], + AUDITBEAT_INSTRUCTIONS.CONFIG[platform], + AUDITBEAT_INSTRUCTIONS.START[platform], + ], + }); + } + + return { + instructionSets: [ + { + title: i18n.translate('kbn.common.tutorials.auditbeat.premCloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), + instructionVariants: variants, + statusCheck: auditbeatStatusCheck(), + }, + ], + }; +} + +export function cloudInstructions(platforms) { + const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(); + const AUDITBEAT_CLOUD_INSTRUCTIONS = createAuditbeatCloudInstructions(); + + const variants = []; + for (let i = 0; i < platforms.length; i++) { + const platform = platforms[i]; + variants.push({ + id: INSTRUCTION_VARIANT[platform], + instructions: [ + AUDITBEAT_INSTRUCTIONS.INSTALL[platform], + AUDITBEAT_CLOUD_INSTRUCTIONS.CONFIG[platform], + AUDITBEAT_INSTRUCTIONS.START[platform], + ], + }); + } + + return { + instructionSets: [ + { + title: i18n.translate('kbn.common.tutorials.auditbeat.cloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), + instructionVariants: variants, + statusCheck: auditbeatStatusCheck(), + }, + ], + }; +} diff --git a/src/legacy/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js b/src/legacy/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js index 088038ab5beb1..d0ffa0d98df20 100644 --- a/src/legacy/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js +++ b/src/legacy/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; export const createTrycloudOption1 = () => ({ title: i18n.translate('kbn.common.tutorials.premCloudInstructions.option1.title', { - defaultMessage: 'Option 1: Try module in Elastic Cloud', + defaultMessage: 'Option 1: Try in Elastic Cloud', }), textPre: i18n.translate('kbn.common.tutorials.premCloudInstructions.option1.textPre', { defaultMessage: 'Go to [Elastic Cloud]({link}). Register if you \ diff --git a/src/legacy/core_plugins/kibana/common/tutorials/winlogbeat_instructions.js b/src/legacy/core_plugins/kibana/common/tutorials/winlogbeat_instructions.js new file mode 100644 index 0000000000000..6573c7ef2adf9 --- /dev/null +++ b/src/legacy/core_plugins/kibana/common/tutorials/winlogbeat_instructions.js @@ -0,0 +1,237 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { INSTRUCTION_VARIANT } from './instruction_variant'; +import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; +import { getSpaceIdForBeatsTutorial } from '../lib/get_space_id_for_beats_tutorial'; + +export const createWinlogbeatInstructions = context => ({ + INSTALL: { + WINDOWS: { + title: i18n.translate('kbn.common.tutorials.winlogbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Winlogbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.winlogbeatInstructions.install.windowsTextPre', { + defaultMessage: 'First time using Winlogbeat? See the [Getting Started Guide]({winlogbeatLink}).\n\ + 1. Download the Winlogbeat Windows zip file from the [Download]({elasticLink}) page.\n\ + 2. Extract the contents of the zip file into {folderPath}.\n\ + 3. Rename the {directoryName} directory to `Winlogbeat`.\n\ + 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ +**Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ + 5. From the PowerShell prompt, run the following commands to install Winlogbeat as a Windows service.', + values: { + directoryName: '`winlogbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + winlogbeatLink: '{config.docs.beats.winlogbeat}/winlogbeat-getting-started.html', + elasticLink: 'https://www.elastic.co/downloads/beats/winlogbeat', + }, + }), + commands: [ + 'cd "C:\\Program Files\\Winlogbeat"', + '.\\install-service-winlogbeat.ps1', + ], + textPost: i18n.translate('kbn.common.tutorials.winlogbeatInstructions.install.windowsTextPost', { + defaultMessage: 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', + values: { path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`' }, + }), + } + }, + START: { + WINDOWS: { + title: i18n.translate('kbn.common.tutorials.winlogbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Winlogbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.winlogbeatInstructions.start.windowsTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + '.\\winlogbeat.exe setup', + 'Start-Service winlogbeat', + ], + }, + }, + CONFIG: { + WINDOWS: { + title: i18n.translate('kbn.common.tutorials.winlogbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.winlogbeatInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context) + ], + textPost: i18n.translate('kbn.common.tutorials.winlogbeatInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), + } + } +}); + +export const createWinlogbeatCloudInstructions = () => ({ + CONFIG: { + WINDOWS: { + title: i18n.translate('kbn.common.tutorials.winlogbeatCloudInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.winlogbeatCloudInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.winlogbeatCloudInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), + } + } +}); + +export function winlogbeatStatusCheck() { + return { + title: i18n.translate('kbn.common.tutorials.winlogbeatStatusCheck.title', { + defaultMessage: 'Module status', + }), + text: i18n.translate('kbn.common.tutorials.winlogbeatStatusCheck.text', { + defaultMessage: 'Check that data is received from Winlogbeat', + }), + btnLabel: i18n.translate('kbn.common.tutorials.winlogbeatStatusCheck.buttonLabel', { + defaultMessage: 'Check data', + }), + success: i18n.translate('kbn.common.tutorials.winlogbeatStatusCheck.successText', { + defaultMessage: 'Data successfully received', + }), + error: i18n.translate('kbn.common.tutorials.winlogbeatStatusCheck.errorText', { + defaultMessage: 'No data has been received yet', + }), + esHitsCheck: { + index: 'winlogbeat-*', + query: { + bool: { + filter: { + term: { + 'agent.type': 'winlogbeat', + }, + }, + }, + }, + }, + }; +} + +export function onPremInstructions(platforms, context) { + const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(context); + + return { + instructionSets: [ + { + title: i18n.translate('kbn.common.tutorials.winlogbeat.premInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + WINLOGBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + WINLOGBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + WINLOGBEAT_INSTRUCTIONS.START.WINDOWS, + ], + }, + ], + statusCheck: winlogbeatStatusCheck(), + }, + ], + }; +} + +export function onPremCloudInstructions() { + const TRYCLOUD_OPTION1 = createTrycloudOption1(); + const TRYCLOUD_OPTION2 = createTrycloudOption2(); + const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(); + + return { + instructionSets: [ + { + title: i18n.translate('kbn.common.tutorials.winlogbeat.premCloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + TRYCLOUD_OPTION1, + TRYCLOUD_OPTION2, + WINLOGBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + WINLOGBEAT_INSTRUCTIONS.CONFIG.WINDOWS, + WINLOGBEAT_INSTRUCTIONS.START.WINDOWS, + ], + }, + ], + statusCheck: winlogbeatStatusCheck(), + }, + ], + }; +} + +export function cloudInstructions() { + const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(); + const WINLOGBEAT_CLOUD_INSTRUCTIONS = createWinlogbeatCloudInstructions(); + + return { + instructionSets: [ + { + title: i18n.translate('kbn.common.tutorials.winlogbeat.cloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), + instructionVariants: [ + { + id: INSTRUCTION_VARIANT.WINDOWS, + instructions: [ + WINLOGBEAT_INSTRUCTIONS.INSTALL.WINDOWS, + WINLOGBEAT_CLOUD_INSTRUCTIONS.CONFIG.WINDOWS, + WINLOGBEAT_INSTRUCTIONS.START.WINDOWS, + ], + }, + ], + statusCheck: winlogbeatStatusCheck(), + }, + ], + }; +} diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 61e6051634863..d58ebec0be334 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -291,6 +291,7 @@ export default function (kibana) { kibana: { settings: true, index_patterns: true, + objects: true, }, } }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx index dd059c47475c3..065f073b0177d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx @@ -37,9 +37,6 @@ export function getCustomizePanelAction({ }): ContextMenuAction { return new ContextMenuAction( { - displayName: i18n.translate('kbn.dashboard.panel.customizePanel.displayName', { - defaultMessage: 'Customize panel', - }), id: 'customizePanel', parentPanelId: 'mainMenu', }, @@ -64,6 +61,11 @@ export function getCustomizePanelAction({ ), icon: , isVisible: ({ containerState }) => containerState.viewMode === DashboardViewMode.EDIT, + getDisplayName: () => { + return i18n.translate('kbn.dashboard.panel.customizePanel.displayName', { + defaultMessage: 'Customize panel', + }); + }, } ); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx index 42a5ddd1e82af..4441b4101e961 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx @@ -32,9 +32,6 @@ import { DashboardViewMode } from '../../../dashboard_view_mode'; export function getEditPanelAction() { return new ContextMenuAction( { - displayName: i18n.translate('kbn.dashboard.panel.editPanel.displayName', { - defaultMessage: 'Edit visualization', - }), id: 'editPanel', parentPanelId: 'mainMenu', }, @@ -54,6 +51,15 @@ export function getEditPanelAction() { return embeddable.metadata.editUrl; } }, + getDisplayName: ({ embeddable }) => { + if (embeddable && embeddable.metadata.editLabel) { + return embeddable.metadata.editLabel; + } + + return i18n.translate('kbn.dashboard.panel.editPanel.defaultDisplayName', { + defaultMessage: 'Edit', + }); + }, } ); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx index 0a995310ae0e5..af8918fe99609 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx @@ -40,12 +40,14 @@ export function getInspectorPanelAction({ return new ContextMenuAction( { id: 'openInspector', - displayName: i18n.translate('kbn.dashboard.panel.inspectorPanel.displayName', { - defaultMessage: 'Inspect', - }), parentPanelId: 'mainMenu', }, { + getDisplayName: () => { + return i18n.translate('kbn.dashboard.panel.inspectorPanel.displayName', { + defaultMessage: 'Inspect', + }); + }, icon: , isVisible: ({ embeddable }) => { if (!embeddable) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx index 47718a5f21b31..113079aaeb103 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx @@ -32,13 +32,15 @@ import { DashboardViewMode } from '../../../dashboard_view_mode'; export function getRemovePanelAction(onDeletePanel: () => void) { return new ContextMenuAction( { - displayName: i18n.translate('kbn.dashboard.panel.removePanel.displayName', { - defaultMessage: 'Delete from dashboard', - }), id: 'deletePanel', parentPanelId: 'mainMenu', }, { + getDisplayName: () => { + return i18n.translate('kbn.dashboard.panel.removePanel.displayName', { + defaultMessage: 'Delete from dashboard', + }); + }, icon: , isVisible: ({ containerState }) => containerState.viewMode === DashboardViewMode.EDIT && !containerState.isPanelExpanded, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx index 80d43347b308c..71dd5598cad26 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx @@ -38,17 +38,19 @@ export function getToggleExpandPanelAction({ }) { return new ContextMenuAction( { - displayName: isExpanded - ? i18n.translate('kbn.dashboard.panel.toggleExpandPanel.expandedDisplayName', { - defaultMessage: 'Minimize', - }) - : i18n.translate('kbn.dashboard.panel.toggleExpandPanel.notExpandedDisplayName', { - defaultMessage: 'Full screen', - }), id: 'togglePanel', parentPanelId: 'mainMenu', }, { + getDisplayName: () => { + return isExpanded + ? i18n.translate('kbn.dashboard.panel.toggleExpandPanel.expandedDisplayName', { + defaultMessage: 'Minimize', + }) + : i18n.translate('kbn.dashboard.panel.toggleExpandPanel.notExpandedDisplayName', { + defaultMessage: 'Full screen', + }); + }, // TODO: Update to minimize icon when https://github.com/elastic/eui/issues/837 is complete. icon: , onClick: toggleExpandedPanel, diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index 079fb1f72bf9f..c41092682283d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -19,6 +19,7 @@ import angular from 'angular'; import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import { SearchSource } from 'ui/courier'; import { ContainerState, @@ -89,6 +90,9 @@ export class SearchEmbeddable extends Embeddable { super({ title: savedSearch.title, editUrl, + editLabel: i18n.translate('kbn.embeddable.search.editLabel', { + defaultMessage: 'Edit saved search', + }), editable, indexPatterns: _.compact([savedSearch.searchSource.getField('index')]), }); diff --git a/src/legacy/core_plugins/kibana/public/home/components/tutorial/replace_template_strings.js b/src/legacy/core_plugins/kibana/public/home/components/tutorial/replace_template_strings.js index bfecde8b3ea72..7875c629306c5 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/tutorial/replace_template_strings.js +++ b/src/legacy/core_plugins/kibana/public/home/components/tutorial/replace_template_strings.js @@ -54,7 +54,9 @@ export function replaceTemplateStrings(text, params = {}) { filebeat: documentationLinks.filebeat.base, metricbeat: documentationLinks.metricbeat.base, heartbeat: documentationLinks.heartbeat.base, - functionbeat: documentationLinks.functionbeat.base + functionbeat: documentationLinks.functionbeat.base, + winlogbeat: documentationLinks.winlogbeat.base, + auditbeat: documentationLinks.auditbeat.base, }, logstash: documentationLinks.logstash.base, version: DOC_LINK_VERSION diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/auditbeat/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/auditbeat/screenshot.png new file mode 100644 index 0000000000000..2d70cc274a2e8 Binary files /dev/null and b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/auditbeat/screenshot.png differ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js index 94ffff0c5c6c4..dc135fb19587a 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -87,7 +87,8 @@ function destroyObjectsTable() { uiRoutes .when('/management/kibana/objects', { template: objectIndexHTML, - k7Breadcrumbs: getIndexBreadcrumbs + k7Breadcrumbs: getIndexBreadcrumbs, + requireUICapability: 'management.kibana.objects', }) .when('/management/kibana/objects/:service', { redirectTo: '/management/kibana/objects' diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js index 085edf4b17956..f3d2010fd0c37 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js @@ -39,7 +39,8 @@ const location = 'SavedObject view'; uiRoutes .when('/management/kibana/objects/:service/:id', { template: objectViewHTML, - k7Breadcrumbs: getViewBreadcrumbs + k7Breadcrumbs: getViewBreadcrumbs, + requireUICapability: 'management.kibana.objects', }); uiModules.get('apps/management', ['monospaced.elastic']) @@ -89,7 +90,7 @@ uiModules.get('apps/management', ['monospaced.elastic']) field.type = 'boolean'; field.value = field.value; } else if (_.isPlainObject(field.value)) { - // do something recursive + // do something recursive return _.reduce(field.value, _.partialRight(createField, parents), memo); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 85dd4a40d5e8d..d284bda9405ab 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -30,6 +30,7 @@ import { VisualizeLoaderParams, VisualizeUpdateParams, } from 'ui/visualize/loader/types'; +import { i18n } from '@kbn/i18n'; export interface VisualizeEmbeddableConfiguration { onEmbeddableStateChanged: OnEmbeddableStateChanged; @@ -63,6 +64,9 @@ export class VisualizeEmbeddable extends Embeddable { super({ title: savedVisualization.title, editUrl, + editLabel: i18n.translate('kbn.embeddable.visualize.editLabel', { + defaultMessage: 'Edit visualization', + }), editable, indexPatterns, }); diff --git a/src/legacy/core_plugins/kibana/server/tutorials/auditbeat/index.js b/src/legacy/core_plugins/kibana/server/tutorials/auditbeat/index.js new file mode 100644 index 0000000000000..84ddaa988b462 --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/auditbeat/index.js @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/auditbeat_instructions'; + +export function auditbeatSpecProvider(server, context) { + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; + return { + id: 'auditbeat', + name: i18n.translate('kbn.server.tutorials.auditbeat.nameTitle', { + defaultMessage: 'Auditbeat', + }), + category: TUTORIAL_CATEGORY.SECURITY, + shortDescription: i18n.translate('kbn.server.tutorials.auditbeat.shortDescription', { + defaultMessage: 'Collect audit data from your hosts.', + }), + longDescription: i18n.translate('kbn.server.tutorials.auditbeat.longDescription', { + defaultMessage: 'Use Auditbeat to collect auditing data from your hosts. These include \ +processes, users, logins, sockets information, file accesses, and more. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.auditbeat}/auditbeat-overview.html', + }, + }), + euiIconType: 'securityAnalyticsApp', + artifacts: { + dashboards: [], + application: { + path: '/app/siem', + label: i18n.translate( + 'kbn.server.tutorials.auditbeat.artifacts.dashboards.linkLabel', + { + defaultMessage: 'SIEM App', + } + ), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.auditbeat}/exported-fields.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/auditbeat/screenshot.png', + onPrem: onPremInstructions(platforms, context), + elasticCloud: cloudInstructions(platforms), + onPremElasticCloud: onPremCloudInstructions(platforms) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js index e13dde167af1b..afae92b204290 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/register.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js @@ -57,6 +57,7 @@ import { memcachedMetricsSpecProvider } from './memcached_metrics'; import { muninMetricsSpecProvider } from './munin_metrics'; import { vSphereMetricsSpecProvider } from './vsphere_metrics'; import { windowsMetricsSpecProvider } from './windows_metrics'; +import { windowsEventLogsSpecProvider } from './windows_event_logs'; import { golangMetricsSpecProvider } from './golang_metrics'; import { logstashMetricsSpecProvider } from './logstash_metrics'; import { prometheusMetricsSpecProvider } from './prometheus_metrics'; @@ -70,6 +71,7 @@ import { natsLogsSpecProvider } from './nats_logs'; import { zeekLogsSpecProvider } from './zeek_logs'; import { corednsMetricsSpecProvider } from './coredns_metrics'; import { corednsLogsSpecProvider } from './coredns_logs'; +import { auditbeatSpecProvider } from './auditbeat'; export function registerTutorials(server) { server.registerTutorial(systemLogsSpecProvider); @@ -112,6 +114,7 @@ export function registerTutorials(server) { server.registerTutorial(muninMetricsSpecProvider); server.registerTutorial(vSphereMetricsSpecProvider); server.registerTutorial(windowsMetricsSpecProvider); + server.registerTutorial(windowsEventLogsSpecProvider); server.registerTutorial(golangMetricsSpecProvider); server.registerTutorial(logstashMetricsSpecProvider); server.registerTutorial(prometheusMetricsSpecProvider); @@ -125,4 +128,5 @@ export function registerTutorials(server) { server.registerTutorial(zeekLogsSpecProvider); server.registerTutorial(corednsMetricsSpecProvider); server.registerTutorial(corednsLogsSpecProvider); + server.registerTutorial(auditbeatSpecProvider); } diff --git a/src/legacy/core_plugins/kibana/server/tutorials/windows_event_logs/index.js b/src/legacy/core_plugins/kibana/server/tutorials/windows_event_logs/index.js new file mode 100644 index 0000000000000..eb37466866d2a --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/windows_event_logs/index.js @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/winlogbeat_instructions'; + +export function windowsEventLogsSpecProvider(server, context) { + return { + id: 'windowsEventLogs', + name: i18n.translate('kbn.server.tutorials.windowsEventLogs.nameTitle', { + defaultMessage: 'Windows Event Log', + }), + isBeta: false, + category: TUTORIAL_CATEGORY.SECURITY, + shortDescription: i18n.translate('kbn.server.tutorials.windowsEventLogs.shortDescription', { + defaultMessage: 'Fetch logs from the Windows Event Log.', + }), + longDescription: i18n.translate('kbn.server.tutorials.windowsEventLogs.longDescription', { + defaultMessage: 'Use Winlogbeat to collect the logs from the Windows Event Log. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.winlogbeat}/index.html', + }, + }), + euiIconType: 'logoWindows', + artifacts: { + application: { + label: i18n.translate('kbn.server.tutorials.windowsEventLogs.artifacts.application.label', { + defaultMessage: 'SIEM App', + }), + path: '/app/siem' + }, + dashboards: [], + exportedFields: { + documentationUrl: '{config.docs.beats.winlogbeat}/exported-fields.html' + } + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(null, null, null, context), + elasticCloud: cloudInstructions(), + onPremElasticCloud: onPremCloudInstructions() + }; +} diff --git a/src/legacy/server/http/index.js b/src/legacy/server/http/index.js index 5feab9c89493f..0b9c660742209 100644 --- a/src/legacy/server/http/index.js +++ b/src/legacy/server/http/index.js @@ -31,7 +31,7 @@ export default async function (kbnServer, server, config) { kbnServer.server = new Hapi.Server(kbnServer.newPlatform.params.serverOptions); server = kbnServer.server; - setupBasePathProvider(server, config); + setupBasePathProvider(kbnServer); await registerHapiPlugins(server); diff --git a/src/legacy/server/http/setup_base_path_provider.js b/src/legacy/server/http/setup_base_path_provider.js index caba48c765b02..8cf6cc1fde512 100644 --- a/src/legacy/server/http/setup_base_path_provider.js +++ b/src/legacy/server/http/setup_base_path_provider.js @@ -17,22 +17,14 @@ * under the License. */ -export function setupBasePathProvider(server, config) { - - server.decorate('request', 'setBasePath', function (basePath) { +export function setupBasePathProvider(kbnServer) { + kbnServer.server.decorate('request', 'setBasePath', function (basePath) { const request = this; - if (request.app._basePath) { - throw new Error(`Request basePath was previously set. Setting multiple times is not supported.`); - } - request.app._basePath = basePath; + kbnServer.newPlatform.setup.core.http.setBasePathFor(request, basePath); }); - server.decorate('request', 'getBasePath', function () { + kbnServer.server.decorate('request', 'getBasePath', function () { const request = this; - - const serverBasePath = config.get('server.basePath'); - const requestBasePath = request.app._basePath || ''; - - return `${serverBasePath}${requestBasePath}`; + return kbnServer.newPlatform.setup.core.http.getBasePathFor(request); }); } diff --git a/src/legacy/server/status/server_status.js b/src/legacy/server/status/server_status.js index 54a17464c8e65..162ddf92310fe 100644 --- a/src/legacy/server/status/server_status.js +++ b/src/legacy/server/status/server_status.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import * as states from './states'; import Status from './status'; -import { version } from '../../utils/package_json'; +import { pkg } from '../../utils/package_json'; export default class ServerStatus { constructor(server) { @@ -36,7 +36,7 @@ export default class ServerStatus { } createForPlugin(plugin) { - if (plugin.version === 'kibana') plugin.version = version; + if (plugin.version === 'kibana') plugin.version = pkg.version; const status = this.create(`plugin:${plugin.id}@${plugin.version}`); status.plugin = plugin; return status; diff --git a/src/legacy/server/status/server_status.test.js b/src/legacy/server/status/server_status.test.js index c6e6721c0f926..b783d7fcb90b9 100644 --- a/src/legacy/server/status/server_status.test.js +++ b/src/legacy/server/status/server_status.test.js @@ -119,10 +119,12 @@ describe('ServerStatus class', function () { it('serializes to overall status and individuals', function () { const pluginOne = { id: 'one', version: '1.0.0' }; const pluginTwo = { id: 'two', version: '2.0.0' }; + const pluginThree = { id: 'three', version: 'kibana' }; const service = serverStatus.create('some service'); const p1 = serverStatus.createForPlugin(pluginOne); const p2 = serverStatus.createForPlugin(pluginTwo); + const p3 = serverStatus.createForPlugin(pluginThree); service.green(); p1.yellow(); @@ -131,12 +133,14 @@ describe('ServerStatus class', function () { const json = JSON.parse(JSON.stringify(serverStatus)); expect(json).toHaveProperty('overall'); expect(json.overall.state).toEqual(serverStatus.overall().state); - expect(json.statuses).toHaveLength(3); + expect(json.statuses).toHaveLength(4); const out = status => find(json.statuses, { id: status.id }); expect(out(service)).toHaveProperty('state', 'green'); expect(out(p1)).toHaveProperty('state', 'yellow'); expect(out(p2)).toHaveProperty('state', 'red'); + expect(out(p3)).toHaveProperty('id'); + expect(out(p3).id).not.toContain('undefined'); }); }); diff --git a/src/legacy/ui/public/chrome/api/sub_url_hooks.js b/src/legacy/ui/public/chrome/api/sub_url_hooks.js index 0d43688bb7586..beb762a1235e6 100644 --- a/src/legacy/ui/public/chrome/api/sub_url_hooks.js +++ b/src/legacy/ui/public/chrome/api/sub_url_hooks.js @@ -17,13 +17,15 @@ * under the License. */ +import url from 'url'; + import { getUnhashableStatesProvider, unhashUrl, } from '../../state_management/state_hashing'; export function registerSubUrlHooks(angularModule, internals) { - angularModule.run(($rootScope, Private) => { + angularModule.run(($rootScope, Private, $location) => { const getUnhashableStates = Private(getUnhashableStatesProvider); const subUrlRouteFilter = Private(SubUrlRouteFilterProvider); @@ -39,6 +41,23 @@ export function registerSubUrlHooks(angularModule, internals) { } } + $rootScope.$on('$locationChangeStart', (e, newUrl) => { + // This handler fixes issue #31238 where browser back navigation + // fails due to angular 1.6 parsing url encoded params wrong. + const parsedAbsUrl = url.parse($location.absUrl()); + const absUrlHash = parsedAbsUrl.hash ? parsedAbsUrl.hash.slice(1) : ''; + const decodedAbsUrlHash = decodeURIComponent(absUrlHash); + + const parsedNewUrl = url.parse(newUrl); + const newHash = parsedNewUrl.hash ? parsedNewUrl.hash.slice(1) : ''; + const decodedHash = decodeURIComponent(newHash); + + if (absUrlHash !== newHash && decodedHash === decodedAbsUrlHash) { + // replace the urlencoded hash with the version that angular sees. + $location.url(absUrlHash).replace(); + } + }); + $rootScope.$on('$routeChangeSuccess', onRouteChange); $rootScope.$on('$routeUpdate', onRouteChange); updateSubUrls(); // initialize sub urls diff --git a/src/legacy/ui/public/documentation_links/documentation_links.ts b/src/legacy/ui/public/documentation_links/documentation_links.ts index 838c6c8de5121..88513c5746ab1 100644 --- a/src/legacy/ui/public/documentation_links/documentation_links.ts +++ b/src/legacy/ui/public/documentation_links/documentation_links.ts @@ -37,6 +37,9 @@ export const documentationLinks = { startup: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-starting.html`, exportedFields: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/exported-fields.html`, }, + auditbeat: { + base: `${ELASTIC_WEBSITE_URL}guide/en/beats/auditbeat/${DOC_LINK_VERSION}`, + }, metricbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}`, }, @@ -49,6 +52,9 @@ export const documentationLinks = { functionbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/functionbeat/${DOC_LINK_VERSION}`, }, + winlogbeat: { + base: `${ELASTIC_WEBSITE_URL}guide/en/beats/winlogbeat/${DOC_LINK_VERSION}`, + }, aggs: { date_histogram: `${ELASTIC_DOCS}search-aggregations-bucket-datehistogram-aggregation.html`, date_range: `${ELASTIC_DOCS}search-aggregations-bucket-daterange-aggregation.html`, diff --git a/src/legacy/ui/public/embeddable/context_menu_actions/build_eui_context_menu_panels.ts b/src/legacy/ui/public/embeddable/context_menu_actions/build_eui_context_menu_panels.ts index 600aacc422459..87c3d13c8de93 100644 --- a/src/legacy/ui/public/embeddable/context_menu_actions/build_eui_context_menu_panels.ts +++ b/src/legacy/ui/public/embeddable/context_menu_actions/build_eui_context_menu_panels.ts @@ -141,7 +141,7 @@ function convertPanelActionToContextMenuItem({ embeddable?: Embeddable; }): EuiContextMenuPanelItemDescriptor { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { - name: action.displayName, + name: action.getDisplayName({ embeddable, containerState }), icon: action.icon, panel: _.get(action, 'childContextMenuPanel.id'), disabled: action.isDisabled({ embeddable, containerState }), diff --git a/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_action.ts b/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_action.ts index f59f552cbfd61..019bbce42690f 100644 --- a/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_action.ts +++ b/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_action.ts @@ -49,6 +49,11 @@ interface ContextMenuActionOptions { * Optional icon to display to the left of the action. */ icon?: EuiContextMenuItemIcon; + + /** + * Return display name of the action in the menu + */ + getDisplayName: (actionAPI: PanelActionAPI) => string; } interface ContextMenuButtonOptions extends ContextMenuActionOptions { @@ -69,11 +74,6 @@ interface ContextMenuLinkOptions extends ContextMenuActionOptions { interface ContextMenuActionsConfig { id: string; - /** - * Display name of the action in the menu - */ - displayName: string; - /** * Determines which ContextMenuPanel this action is displayed on. */ @@ -88,11 +88,6 @@ export class ContextMenuAction { */ public readonly icon?: EuiContextMenuItemIcon; - /** - * Display name of the action in the menu - */ - public readonly displayName: string; - /** * Optional child context menu to open when the action is clicked. */ @@ -113,28 +108,33 @@ export class ContextMenuAction { */ public readonly getHref?: (panelActionAPI: PanelActionAPI) => string; + /** + * @param {PanelActionAPI} panelActionAPI + */ + public readonly getDisplayName: (panelActionAPI: PanelActionAPI) => string; + /** * * @param {string} config.id - * @param {string} config.displayName * @param {string} config.parentPanelId - set if this action belongs on a nested child panel * @param {function} options.onClick * @param {ContextMenuPanel} options.childContextMenuPanel - optional child panel to open when clicked. * @param {function} options.isDisabled - optionally set a custom disabled function * @param {function} options.isVisible - optionally set a custom isVisible function * @param {function} options.getHref + * @param {function} options.getDisplayName * @param {Element} options.icon */ public constructor( config: ContextMenuActionsConfig, - options: ContextMenuButtonOptions | ContextMenuLinkOptions = {} + options: ContextMenuButtonOptions | ContextMenuLinkOptions ) { this.id = config.id; - this.displayName = config.displayName; this.parentPanelId = config.parentPanelId; this.icon = options.icon; this.childContextMenuPanel = options.childContextMenuPanel; + this.getDisplayName = options.getDisplayName; if ('onClick' in options) { this.onClick = options.onClick; diff --git a/src/legacy/ui/public/embeddable/embeddable.ts b/src/legacy/ui/public/embeddable/embeddable.ts index f70ee5a114bbe..678bd13aebfc8 100644 --- a/src/legacy/ui/public/embeddable/embeddable.ts +++ b/src/legacy/ui/public/embeddable/embeddable.ts @@ -40,6 +40,8 @@ export interface EmbeddableMetadata { */ editUrl?: string; + editLabel?: string; + /** * A flag indicating if this embeddable can be edited. */ diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js index 291eff2454c29..6fcbacede2a65 100644 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js +++ b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js @@ -17,20 +17,18 @@ * under the License. */ -jest.mock('../chrome', () => ({ - addBasePath: path => `myBase/${path}`, -})); -jest.mock('../metadata', () => ({ - metadata: { - version: 'my-version', - }, -})); - +// @ts-ignore import fetchMock from 'fetch-mock/es5/client'; -import { kfetch } from 'ui/kfetch'; +import { __newPlatformSetup__, kfetch } from '../kfetch'; +import { setup } from '../../../../test_utils/public/kfetch_test_setup'; + import { isAutoCreateIndexError } from './error_auto_create_index'; describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch', () => { + beforeAll(() => { + __newPlatformSetup__(setup().http); + }); + describe('404', () => { beforeEach(() => { fetchMock.post({ @@ -45,7 +43,7 @@ describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch' test('should return false', async () => { expect.assertions(1); try { - await kfetch({ method: 'POST', pathname: 'my/path' }); + await kfetch({ method: 'POST', pathname: '/my/path' }); } catch (kfetchError) { expect(isAutoCreateIndexError(kfetchError)).toBe(false); } @@ -66,7 +64,7 @@ describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch' test('should return false', async () => { expect.assertions(1); try { - await kfetch({ method: 'POST', pathname: 'my/path' }); + await kfetch({ method: 'POST', pathname: '/my/path' }); } catch (kfetchError) { expect(isAutoCreateIndexError(kfetchError)).toBe(false); } @@ -90,7 +88,7 @@ describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch' test('should return true', async () => { expect.assertions(1); try { - await kfetch({ method: 'POST', pathname: 'my/path' }); + await kfetch({ method: 'POST', pathname: '/my/path' }); } catch (kfetchError) { expect(isAutoCreateIndexError(kfetchError)).toBe(true); } diff --git a/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.js.snap b/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.js.snap index 1fe571dc17801..41560f8971ae5 100644 --- a/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.js.snap +++ b/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.js.snap @@ -151,13 +151,16 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = ` - @@ -379,14 +382,17 @@ exports[`FieldEditor should render edit scripted field correctly 1`] = ` - @@ -672,13 +678,16 @@ exports[`FieldEditor should show conflict field warning 1`] = ` - @@ -986,14 +995,17 @@ exports[`FieldEditor should show deprecated lang warning 1`] = ` - @@ -1332,13 +1344,16 @@ exports[`FieldEditor should show multiple type field warning with a table contai - diff --git a/src/legacy/ui/public/field_editor/field_editor.js b/src/legacy/ui/public/field_editor/field_editor.js index 364349e400a2d..f8552d32907ec 100644 --- a/src/legacy/ui/public/field_editor/field_editor.js +++ b/src/legacy/ui/public/field_editor/field_editor.js @@ -45,6 +45,7 @@ import { EuiButtonEmpty, EuiCallOut, EuiCode, + EuiCodeEditor, EuiConfirmModal, EuiFieldNumber, EuiFieldText, @@ -58,7 +59,6 @@ import { EuiSelect, EuiSpacer, EuiText, - EuiTextArea, EUI_MODAL_CONFIRM_BUTTON, } from '@elastic/eui'; @@ -80,6 +80,9 @@ import { copyField, getDefaultFormat, executeScript, isScriptValid } from './lib import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +// This loads Ace editor's "groovy" mode, used below to highlight the script. +import 'brace/mode/groovy'; + export class FieldEditorComponent extends PureComponent { static propTypes = { indexPattern: PropTypes.object.isRequired, @@ -459,11 +462,11 @@ export class FieldEditorComponent extends PureComponent { ); } - onScriptChange = (e) => { + onScriptChange = (value) => { this.setState({ hasScriptError: false }); - this.onFieldChange('script', e.target.value); + this.onFieldChange('script', value); } renderScript() { @@ -481,15 +484,18 @@ export class FieldEditorComponent extends PureComponent { return field.scripted ? ( - diff --git a/src/legacy/ui/public/field_editor/field_editor.test.js b/src/legacy/ui/public/field_editor/field_editor.test.js index b8f745bd77fc7..60c20719a860b 100644 --- a/src/legacy/ui/public/field_editor/field_editor.test.js +++ b/src/legacy/ui/public/field_editor/field_editor.test.js @@ -22,6 +22,8 @@ jest.mock('ui/kfetch', () => ({})); import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +jest.mock('brace/mode/groovy', () => ({})); + import { FieldEditorComponent } from './field_editor'; jest.mock('@elastic/eui', () => ({ diff --git a/src/legacy/ui/public/inspector/inspector.tsx b/src/legacy/ui/public/inspector/inspector.tsx index 6f1b7d46dff1f..88501841cb868 100644 --- a/src/legacy/ui/public/inspector/inspector.tsx +++ b/src/legacy/ui/public/inspector/inspector.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { FlyoutRef } from '../../../../core/public'; +import { OverlayRef } from '../../../../core/public'; import { getNewPlatform } from '../new_platform'; import { Adapters } from './types'; import { InspectorPanel } from './ui/inspector_panel'; @@ -50,7 +50,7 @@ interface InspectorOptions { title?: string; } -export type InspectorSession = FlyoutRef; +export type InspectorSession = OverlayRef; /** * Opens the inspector panel for the given adapters and close any previously opened diff --git a/src/legacy/ui/public/kfetch/_import_objects.ndjson b/src/legacy/ui/public/kfetch/_import_objects.ndjson new file mode 100644 index 0000000000000..3511fb44cdfb2 --- /dev/null +++ b/src/legacy/ui/public/kfetch/_import_objects.ndjson @@ -0,0 +1 @@ +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Log Agents","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"082f1d60-a2e7-11e7-bb30-233be9be6a15","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1} diff --git a/src/legacy/ui/public/kfetch/index.ts b/src/legacy/ui/public/kfetch/index.ts index 234304b5950aa..011321c83c4cd 100644 --- a/src/legacy/ui/public/kfetch/index.ts +++ b/src/legacy/ui/public/kfetch/index.ts @@ -17,5 +17,23 @@ * under the License. */ -export { kfetch, addInterceptor, KFetchOptions, KFetchQuery } from './kfetch'; -export { kfetchAbortable } from './kfetch_abortable'; +import { createKfetch, KFetchKibanaOptions, KFetchOptions } from './kfetch'; +export { addInterceptor, KFetchOptions, KFetchQuery } from './kfetch'; + +import { HttpSetup } from '../../../../core/public'; + +let http: HttpSetup; +let kfetchInstance: (options: KFetchOptions, kfetchOptions?: KFetchKibanaOptions) => any; + +export function __newPlatformSetup__(httpSetup: HttpSetup) { + if (http) { + throw new Error('ui/kfetch already initialized with New Platform APIs'); + } + + http = httpSetup; + kfetchInstance = createKfetch(http); +} + +export const kfetch = (options: KFetchOptions, kfetchOptions?: KFetchKibanaOptions) => { + return kfetchInstance(options, kfetchOptions); +}; diff --git a/src/legacy/ui/public/kfetch/kfetch.test.ts b/src/legacy/ui/public/kfetch/kfetch.test.ts index 8f8cc807911a3..79bca9da1d273 100644 --- a/src/legacy/ui/public/kfetch/kfetch.test.ts +++ b/src/legacy/ui/public/kfetch/kfetch.test.ts @@ -17,28 +17,20 @@ * under the License. */ -jest.mock('../chrome', () => ({ - addBasePath: (path: string) => `http://localhost/myBase/${path}`, -})); - -jest.mock('../metadata', () => ({ - metadata: { - version: 'my-version', - }, -})); - // @ts-ignore import fetchMock from 'fetch-mock/es5/client'; -import { - addInterceptor, - Interceptor, - kfetch, - resetInterceptors, - withDefaultOptions, -} from './kfetch'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { __newPlatformSetup__, addInterceptor, kfetch, KFetchOptions } from '.'; +import { Interceptor, resetInterceptors, withDefaultOptions } from './kfetch'; import { KFetchError } from './kfetch_error'; +import { setup } from '../../../../test_utils/public/kfetch_test_setup'; describe('kfetch', () => { + beforeAll(() => { + __newPlatformSetup__(setup().http); + }); + afterEach(() => { fetchMock.restore(); resetInterceptors(); @@ -46,13 +38,13 @@ describe('kfetch', () => { it('should use supplied request method', async () => { fetchMock.post('*', {}); - await kfetch({ pathname: 'my/path', method: 'POST' }); + await kfetch({ pathname: '/my/path', method: 'POST' }); expect(fetchMock.lastOptions()!.method).toBe('POST'); }); it('should use supplied Content-Type', async () => { fetchMock.get('*', {}); - await kfetch({ pathname: 'my/path', headers: { 'Content-Type': 'CustomContentType' } }); + await kfetch({ pathname: '/my/path', headers: { 'Content-Type': 'CustomContentType' } }); expect(fetchMock.lastOptions()!.headers).toMatchObject({ 'Content-Type': 'CustomContentType', }); @@ -60,64 +52,88 @@ describe('kfetch', () => { it('should use supplied pathname and querystring', async () => { fetchMock.get('*', {}); - await kfetch({ pathname: 'my/path', query: { a: 'b' } }); + await kfetch({ pathname: '/my/path', query: { a: 'b' } }); expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); }); it('should use supplied headers', async () => { fetchMock.get('*', {}); await kfetch({ - pathname: 'my/path', + pathname: '/my/path', headers: { myHeader: 'foo' }, }); expect(fetchMock.lastOptions()!.headers).toEqual({ 'Content-Type': 'application/json', - 'kbn-version': 'my-version', + 'kbn-version': 'kibanaVersion', myHeader: 'foo', }); }); it('should return response', async () => { fetchMock.get('*', { foo: 'bar' }); - const res = await kfetch({ pathname: 'my/path' }); + const res = await kfetch({ pathname: '/my/path' }); expect(res).toEqual({ foo: 'bar' }); }); it('should prepend url with basepath by default', async () => { fetchMock.get('*', {}); - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); }); it('should not prepend url with basepath when disabled', async () => { fetchMock.get('*', {}); - await kfetch({ pathname: 'my/path' }, { prependBasePath: false }); + await kfetch({ pathname: '/my/path' }, { prependBasePath: false }); expect(fetchMock.lastUrl()).toBe('/my/path'); }); it('should make request with defaults', async () => { fetchMock.get('*', {}); - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); - expect(fetchMock.lastOptions()!).toEqual({ + expect(fetchMock.lastOptions()!).toMatchObject({ method: 'GET', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', - 'kbn-version': 'my-version', + 'kbn-version': 'kibanaVersion', }, }); }); + it('should make requests for NDJSON content', async () => { + const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); + + fetchMock.post('*', { + body: content, + headers: { 'Content-Type': 'application/ndjson' }, + }); + + const data = await kfetch({ + method: 'POST', + pathname: '/my/path', + body: content, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + expect(data).toBeInstanceOf(Blob); + + const ndjson = await new Response(data).text(); + + expect(ndjson).toEqual(content); + }); + it('should reject on network error', async () => { expect.assertions(1); - fetchMock.get('*', { throws: new Error('Network issue') }); + fetchMock.get('*', { status: 500 }); try { - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); } catch (e) { - expect(e.message).toBe('Network issue'); + expect(e.message).toBe('Internal Server Error'); } }); @@ -126,7 +142,7 @@ describe('kfetch', () => { beforeEach(async () => { fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); try { - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); } catch (e) { error = e; } @@ -154,7 +170,7 @@ describe('kfetch', () => { fetchMock.get('*', { foo: 'bar' }); interceptorCalls = mockInterceptorCalls([{}, {}, {}]); - resp = await kfetch({ pathname: 'my/path' }); + resp = await kfetch({ pathname: '/my/path' }); }); it('should call interceptors in correct order', () => { @@ -185,12 +201,12 @@ describe('kfetch', () => { fetchMock.get('*', { foo: 'bar' }); interceptorCalls = mockInterceptorCalls([ - { requestError: () => ({}) }, + { requestError: () => ({ pathname: '/my/path' } as KFetchOptions) }, { request: () => Promise.reject(new Error('Error in request')) }, {}, ]); - resp = await kfetch({ pathname: 'my/path' }); + resp = await kfetch({ pathname: '/my/path' }); }); it('should call interceptors in correct order', () => { @@ -227,7 +243,7 @@ describe('kfetch', () => { ]); try { - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); } catch (e) { error = e; } @@ -267,7 +283,7 @@ describe('kfetch', () => { ]); try { - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); } catch (e) { error = e; } @@ -313,7 +329,7 @@ describe('kfetch', () => { {}, ]); - resp = await kfetch({ pathname: 'my/path' }); + resp = await kfetch({ pathname: '/my/path' }); }); it('should call in correct order', () => { @@ -351,7 +367,7 @@ describe('kfetch', () => { }), }); - resp = await kfetch({ pathname: 'my/path' }); + resp = await kfetch({ pathname: '/my/path' }); }); it('should modify request', () => { @@ -386,7 +402,7 @@ describe('kfetch', () => { }), }); - resp = await kfetch({ pathname: 'my/path' }); + resp = await kfetch({ pathname: '/my/path' }); }); it('should modify request', () => { @@ -453,6 +469,7 @@ function mockInterceptorCalls(interceptors: Interceptor[]) { describe('withDefaultOptions', () => { it('should remove undefined query params', () => { const { query } = withDefaultOptions({ + pathname: '/withDefaultOptions', query: { foo: 'bar', param1: (undefined as any) as string, @@ -464,9 +481,10 @@ describe('withDefaultOptions', () => { }); it('should add default options', () => { - expect(withDefaultOptions({})).toEqual({ + expect(withDefaultOptions({ pathname: '/addDefaultOptions' })).toEqual({ + pathname: '/addDefaultOptions', credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', 'kbn-version': 'my-version' }, + headers: { 'Content-Type': 'application/json' }, method: 'GET', }); }); diff --git a/src/legacy/ui/public/kfetch/kfetch.ts b/src/legacy/ui/public/kfetch/kfetch.ts index 93d736bd8666e..cb96e03eb1328 100644 --- a/src/legacy/ui/public/kfetch/kfetch.ts +++ b/src/legacy/ui/public/kfetch/kfetch.ts @@ -19,17 +19,18 @@ import { merge } from 'lodash'; // @ts-ignore not really worth typing -import { metadata } from 'ui/metadata'; -import url from 'url'; -import chrome from '../chrome'; import { KFetchError } from './kfetch_error'; +import { HttpSetup } from '../../../../core/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { HttpRequestInit } from '../../../../core/public/http/types'; + export interface KFetchQuery { [key: string]: string | number | boolean | undefined; } -export interface KFetchOptions extends RequestInit { - pathname?: string; +export interface KFetchOptions extends HttpRequestInit { + pathname: string; query?: KFetchQuery; } @@ -48,32 +49,21 @@ const interceptors: Interceptor[] = []; export const resetInterceptors = () => (interceptors.length = 0); export const addInterceptor = (interceptor: Interceptor) => interceptors.push(interceptor); -export async function kfetch( - options: KFetchOptions, - { prependBasePath = true }: KFetchKibanaOptions = {} -) { - const combinedOptions = withDefaultOptions(options); - const promise = requestInterceptors(combinedOptions).then( - ({ pathname, query, ...restOptions }) => { - const fullUrl = url.format({ - pathname: prependBasePath ? chrome.addBasePath(pathname) : pathname, - query, - }); - - return window.fetch(fullUrl, restOptions).then(async res => { - if (!res.ok) { - throw new KFetchError(res, await getBodyAsJson(res)); - } - const contentType = res.headers.get('content-type'); - if (contentType && contentType.split(';')[0] === 'application/ndjson') { - return await getBodyAsBlob(res); - } - return await getBodyAsJson(res); - }); - } - ); - - return responseInterceptors(promise); +export function createKfetch(http: HttpSetup) { + return function kfetch( + options: KFetchOptions, + { prependBasePath = true }: KFetchKibanaOptions = {} + ) { + return responseInterceptors( + requestInterceptors(withDefaultOptions(options)) + .then(({ pathname, ...restOptions }) => + http.fetch(pathname, { ...restOptions, prependBasePath }) + ) + .catch(err => { + throw new KFetchError(err.response || { statusText: err.message }, err.body); + }) + ); + }; } // Request/response interceptors are called in opposite orders. @@ -91,36 +81,29 @@ function responseInterceptors(responsePromise: Promise) { }, responsePromise); } -async function getBodyAsJson(res: Response) { - try { - return await res.json(); - } catch (e) { - return null; - } -} - -async function getBodyAsBlob(res: Response) { - try { - return await res.blob(); - } catch (e) { - return null; - } -} - export function withDefaultOptions(options?: KFetchOptions): KFetchOptions { - return merge( + const withDefaults = merge( { method: 'GET', credentials: 'same-origin', headers: { - ...(options && options.headers && options.headers.hasOwnProperty('Content-Type') - ? {} - : { - 'Content-Type': 'application/json', - }), - 'kbn-version': metadata.version, + 'Content-Type': 'application/json', }, }, options - ); + ) as KFetchOptions; + + if ( + options && + options.headers && + 'Content-Type' in options.headers && + options.headers['Content-Type'] === undefined + ) { + // TS thinks headers could be undefined here, but that isn't possible because + // of the merge above. + // @ts-ignore + withDefaults.headers['Content-Type'] = undefined; + } + + return withDefaults; } diff --git a/src/test_utils/public/kfetch_test_setup.ts b/src/test_utils/public/kfetch_test_setup.ts new file mode 100644 index 0000000000000..a102ceb89faf2 --- /dev/null +++ b/src/test_utils/public/kfetch_test_setup.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { HttpService } from '../../core/public/http'; +import { BasePathService } from '../../core/public/base_path'; +import { fatalErrorsServiceMock } from '../../core/public/fatal_errors/fatal_errors_service.mock'; +import { injectedMetadataServiceMock } from '../../core/public/injected_metadata/injected_metadata_service.mock'; +/* eslint-enable @kbn/eslint/no-restricted-paths */ + +export function setup() { + const httpService = new HttpService(); + const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + + injectedMetadata.getBasePath.mockReturnValue('http://localhost/myBase'); + + const basePath = new BasePathService().setup({ injectedMetadata }); + const http = httpService.setup({ basePath, fatalErrors, injectedMetadata }); + + return { httpService, fatalErrors, http }; +} diff --git a/test/functional/apps/dashboard/view_edit.js b/test/functional/apps/dashboard/view_edit.js index edef26dbc2d34..1c866cd2267ff 100644 --- a/test/functional/apps/dashboard/view_edit.js +++ b/test/functional/apps/dashboard/view_edit.js @@ -81,7 +81,7 @@ export default function ({ getService, getPageObjects }) { it('when the query is edited and applied', async function () { const originalQuery = await queryBar.getQueryString(); - await queryBar.setQuery(`${originalQuery} and extra stuff`); + await queryBar.setQuery(`${originalQuery}and extra stuff`); await queryBar.submitQuery(); await PageObjects.dashboard.clickCancelOutOfEditMode(); @@ -209,7 +209,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.gotoDashboardEditMode(dashboardName); const originalQuery = await queryBar.getQueryString(); - await queryBar.setQuery(`${originalQuery} extra stuff`); + await queryBar.setQuery(`${originalQuery}extra stuff`); await PageObjects.dashboard.clickCancelOutOfEditMode(); diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 91d81480dbb16..019c128b275ea 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -31,19 +31,17 @@ export default function ({ getService, getPageObjects }) { defaultIndex: 'logstash-*', }; - describe('discover app', function describeIndexTests() { + describe('discover test', function describeIndexTests() { const fromTime = '2015-09-19 06:31:44.000'; const toTime = '2015-09-23 18:31:44.000'; before(async function () { - // delete .kibana index and update configDoc - await kibanaServer.uiSettings.replace(defaultSettings); - log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); // and load a set of makelogs data await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace(defaultSettings); log.debug('discover'); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); @@ -56,6 +54,9 @@ export default function ({ getService, getPageObjects }) { const time = await PageObjects.timePicker.getTimeConfig(); expect(time.start).to.be('Sep 19, 2015 @ 06:31:44.000'); expect(time.end).to.be('Sep 23, 2015 @ 18:31:44.000'); + const rowData = await PageObjects.discover.getDocTableIndex(1); + log.debug('check the newest doc timestamp in UTC (check diff timezone in last test)'); + expect(rowData.startsWith('Sep 22, 2015 @ 23:50:13.253')).to.be.ok(); }); it('save query should show toast message and display query name', async function () { @@ -435,6 +436,7 @@ export default function ({ getService, getPageObjects }) { describe('time zone switch', () => { it('should show bars in the correct time zone after switching', async function () { + await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); await browser.refresh(); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); @@ -457,7 +459,11 @@ export default function ({ getService, getPageObjects }) { } } }); + log.debug('check that the newest doc timestamp is now -7 hours from the UTC time in the first test'); + const rowData = await PageObjects.discover.getDocTableIndex(1); + expect(rowData.startsWith('Sep 22, 2015 @ 16:50:13.253')).to.be.ok(); }); + }); }); } diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index 3e901388ed383..2bb55434a9299 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -29,6 +29,10 @@ // 3. Filter in Discover by the scripted field // 4. Visualize with aggregation on the scripted field by clicking discover.clickFieldListItemVisualize +// NOTE: Scripted field input is managed by Ace editor, which automatically +// appends closing braces, for exmaple, if you type opening square brace [ +// it will automatically insert a a closing square brace ], etc. + import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { @@ -65,7 +69,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.clickScriptedFieldsTab(); await PageObjects.settings.clickAddScriptedField(); await PageObjects.settings.setScriptedFieldName('doomedScriptedField'); - await PageObjects.settings.setScriptedFieldScript(`doc['iHaveNoClosingTick].value`); + await PageObjects.settings.setScriptedFieldScript(`i n v a l i d s c r i p t`); await PageObjects.settings.clickSaveScriptedField(); await retry.try(async () => { const invalidScriptErrorExists = await testSubjects.exists('invalidScriptError'); @@ -110,11 +114,9 @@ export default function ({ getService, getPageObjects }) { const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); - const script = `if (doc['machine.ram'].size() == 0) { - return -1; - } else { - return doc['machine.ram'].value / (1024 * 1024 * 1024); - }`; + const script = `if (doc['machine.ram'].size() == 0) return -1; + else return doc['machine.ram'].value / (1024 * 1024 * 1024); + `; await PageObjects.settings.addScriptedField(scriptedPainlessFieldName, 'painless', 'number', null, '1', script); await retry.try(async function () { expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be(startingCount + 1); diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.js index f6f2de7c793d1..13287f4f92965 100644 --- a/test/functional/apps/management/_scripted_fields_preview.js +++ b/test/functional/apps/management/_scripted_fields_preview.js @@ -45,7 +45,7 @@ export default function ({ getService, getPageObjects }) { }); it('should display script error when script is invalid', async function () { - const scriptResults = await PageObjects.settings.executeScriptedField(`doc['iHaveNoClosingTick].value`); + const scriptResults = await PageObjects.settings.executeScriptedField(`i n v a l i d s c r i p t`); expect(scriptResults).to.contain('search_phase_execution_exception'); }); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 8a1f823239705..2c4eb59b638fe 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -24,6 +24,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const log = getService('log'); + const retry = getService('retry'); const inspector = getService('inspector'); const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); @@ -42,8 +43,10 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should show the correct count in the legend', async () => { - const actualCount = await PageObjects.visualBuilder.getRhythmChartLegendValue(); - expect(actualCount).to.be('156'); + await retry.try(async () => { + const actualCount = await PageObjects.visualBuilder.getRhythmChartLegendValue(); + expect(actualCount).to.be('156'); + }); }); it('should show the correct count in the legend with 2h offset', async () => { diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.js index 207b74670e38c..872a07a0ff3e0 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.js @@ -510,13 +510,12 @@ export function SettingsPageProvider({ getService, getPageObjects }) { async setScriptedFieldScript(script) { log.debug('set scripted field script = ' + script); - const field = await testSubjects.find('editorFieldScript'); - const currentValue = await field.getAttribute('value'); - if (script === currentValue) { - return; + const aceEditorCssSelector = '[data-test-subj="codeEditorContainer"] .ace_editor'; + await find.clickByCssSelector(aceEditorCssSelector); + for (let i = 0; i < 1000; i++) { + await browser.pressKeys(browser.keys.BACK_SPACE); } - await field.clearValueWithKeyboard({ charByChar: true }); - await field.type(script); + await browser.pressKeys(...script.split('')); } async openScriptedFieldHelp(activeTab) { diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index adfd2ae086e7a..080532187b70a 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -165,10 +165,9 @@ export class WebElementWrapper { await delay(100); } } else { - const selectionKey = this.Keys[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL']; - await this.pressKeys([selectionKey, 'a']); - await this.pressKeys(this.Keys.NULL); // Release modifier keys - await this.pressKeys(this.Keys.BACK_SPACE); // Delete all content + // https://bugs.chromium.org/p/chromedriver/issues/detail?id=30 + await this.driver.executeScript(`arguments[0].select();`, this._webElement); + await this.pressKeys(this.Keys.BACK_SPACE); } } diff --git a/test/functional/services/query_bar.js b/test/functional/services/query_bar.js index 1cc525fbb6164..1652ac2144953 100644 --- a/test/functional/services/query_bar.js +++ b/test/functional/services/query_bar.js @@ -22,6 +22,7 @@ export function QueryBarProvider({ getService, getPageObjects }) { const retry = getService('retry'); const log = getService('log'); const PageObjects = getPageObjects(['header', 'common']); + const find = getService('find'); class QueryBar { @@ -34,7 +35,13 @@ export function QueryBarProvider({ getService, getPageObjects }) { // Extra caution used because of flaky test here: https://github.com/elastic/kibana/issues/16978 doesn't seem // to be actually setting the query in the query input based off await retry.try(async () => { - await testSubjects.setValue('queryInput', query); + await testSubjects.click('queryInput'); + + // testSubjects.setValue uses input.clearValue which wasn't working, but input.clearValueWithKeyboard does. + // So the following lines do the same thing as input.setValue but with input.clearValueWithKeyboard instead. + const input = await find.activeElement(); + await input.clearValueWithKeyboard(); + await input.type(query); const currentQuery = await this.getQueryString(); if (currentQuery !== query) { throw new Error(`Failed to set query input to ${query}, instead query is ${currentQuery}`); diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx index 528bf16292f7c..d8399fa3618fb 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx @@ -28,11 +28,17 @@ import { class SamplePanelAction extends ContextMenuAction { constructor() { - super({ - displayName: 'Sample Panel Action', - id: 'samplePanelAction', - parentPanelId: 'mainMenu', - }); + super( + { + id: 'samplePanelAction', + parentPanelId: 'mainMenu', + }, + { + getDisplayName: () => { + return 'Sample Panel Action'; + }, + } + ); } public onClick = ({ embeddable }: PanelActionAPI) => { if (!embeddable) { diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts index 7092c783125d9..ea754dc269d03 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts @@ -20,11 +20,17 @@ import { ContextMenuAction, ContextMenuActionsRegistryProvider } from 'ui/embedd class SamplePanelLink extends ContextMenuAction { constructor() { - super({ - displayName: 'Sample Panel Link', - id: 'samplePanelLink', - parentPanelId: 'mainMenu', - }); + super( + { + id: 'samplePanelLink', + parentPanelId: 'mainMenu', + }, + { + getDisplayName: () => { + return 'Sample Panel Link'; + }, + } + ); } public getHref = () => { diff --git a/x-pack/package.json b/x-pack/package.json index 773e642dd4ed1..b2da1c78d0071 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -170,7 +170,7 @@ "@elastic/javascript-typescript-langserver": "^0.1.23", "@elastic/lsp-extension": "^0.1.1", "@elastic/node-crypto": "^1.0.0", - "@elastic/nodegit": "0.25.0-alpha.19", + "@elastic/nodegit": "0.25.0-alpha.20", "@elastic/numeral": "2.3.3", "@kbn/babel-preset": "1.0.0", "@kbn/elastic-idx": "1.0.0", @@ -178,6 +178,7 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/ui-framework": "1.0.0", + "@mapbox/mapbox-gl-draw": "^1.1.1", "@samverschueren/stream-to-observable": "^0.3.0", "@scant/router": "^0.1.0", "@slack/client": "^4.8.0", @@ -255,6 +256,7 @@ "lodash.uniqby": "^4.7.0", "lz-string": "^1.4.4", "mapbox-gl": "0.52.0", + "mapbox-gl-draw-rectangle-mode": "^1.0.4", "markdown-it": "^8.4.1", "mime": "^2.2.2", "mkdirp": "0.5.1", diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 8e78fe94763b9..219b9803a2d9f 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -12,6 +12,20 @@ exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`; +exports[`Error METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; + +exports[`Error METRIC_JAVA_HEAP_MEMORY_MAX 1`] = `undefined`; + +exports[`Error METRIC_JAVA_HEAP_MEMORY_USED 1`] = `undefined`; + +exports[`Error METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED 1`] = `undefined`; + +exports[`Error METRIC_JAVA_NON_HEAP_MEMORY_MAX 1`] = `undefined`; + +exports[`Error METRIC_JAVA_NON_HEAP_MEMORY_USED 1`] = `undefined`; + +exports[`Error METRIC_JAVA_THREAD_COUNT 1`] = `undefined`; + exports[`Error METRIC_PROCESS_CPU_PERCENT 1`] = `undefined`; exports[`Error METRIC_SYSTEM_CPU_PERCENT 1`] = `undefined`; @@ -74,6 +88,20 @@ exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`; +exports[`Span METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; + +exports[`Span METRIC_JAVA_HEAP_MEMORY_MAX 1`] = `undefined`; + +exports[`Span METRIC_JAVA_HEAP_MEMORY_USED 1`] = `undefined`; + +exports[`Span METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED 1`] = `undefined`; + +exports[`Span METRIC_JAVA_NON_HEAP_MEMORY_MAX 1`] = `undefined`; + +exports[`Span METRIC_JAVA_NON_HEAP_MEMORY_USED 1`] = `undefined`; + +exports[`Span METRIC_JAVA_THREAD_COUNT 1`] = `undefined`; + exports[`Span METRIC_PROCESS_CPU_PERCENT 1`] = `undefined`; exports[`Span METRIC_SYSTEM_CPU_PERCENT 1`] = `undefined`; @@ -136,6 +164,20 @@ exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`; +exports[`Transaction METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; + +exports[`Transaction METRIC_JAVA_HEAP_MEMORY_MAX 1`] = `undefined`; + +exports[`Transaction METRIC_JAVA_HEAP_MEMORY_USED 1`] = `undefined`; + +exports[`Transaction METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED 1`] = `undefined`; + +exports[`Transaction METRIC_JAVA_NON_HEAP_MEMORY_MAX 1`] = `undefined`; + +exports[`Transaction METRIC_JAVA_NON_HEAP_MEMORY_USED 1`] = `undefined`; + +exports[`Transaction METRIC_JAVA_THREAD_COUNT 1`] = `undefined`; + exports[`Transaction METRIC_PROCESS_CPU_PERCENT 1`] = `undefined`; exports[`Transaction METRIC_SYSTEM_CPU_PERCENT 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index 6644df2e163a1..ebccac0d704f7 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -43,3 +43,12 @@ export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free'; export const METRIC_SYSTEM_TOTAL_MEMORY = 'system.memory.total'; export const METRIC_SYSTEM_CPU_PERCENT = 'system.cpu.total.norm.pct'; export const METRIC_PROCESS_CPU_PERCENT = 'system.process.cpu.total.norm.pct'; + +export const METRIC_JAVA_HEAP_MEMORY_MAX = 'jvm.memory.heap.max'; +export const METRIC_JAVA_HEAP_MEMORY_COMMITTED = 'jvm.memory.heap.committed'; +export const METRIC_JAVA_HEAP_MEMORY_USED = 'jvm.memory.heap.used'; +export const METRIC_JAVA_NON_HEAP_MEMORY_MAX = 'jvm.memory.non_heap.max'; +export const METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED = + 'jvm.memory.non_heap.committed'; +export const METRIC_JAVA_NON_HEAP_MEMORY_USED = 'jvm.memory.non_heap.used'; +export const METRIC_JAVA_THREAD_COUNT = 'jvm.thread.count'; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 942dd673c688d..930c7e21a26e2 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -186,7 +186,7 @@ export function ErrorGroupDetails() { )} /> - + {showDetails && ( = ({
- + diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/CPUUsageChart.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/CPUUsageChart.tsx deleted file mode 100644 index e4e130bce570a..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/CPUUsageChart.tsx +++ /dev/null @@ -1,48 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { Coordinate } from '../../../../typings/timeseries'; -import { CPUMetricSeries } from '../../../selectors/chartSelectors'; -import { asPercent } from '../../../utils/formatters'; -// @ts-ignore -import CustomPlot from '../../shared/charts/CustomPlot'; -import { HoverXHandlers } from '../../shared/charts/SyncChartGroup'; - -interface Props { - data: CPUMetricSeries; - hoverXHandlers: HoverXHandlers; -} - -const tickFormatY = (y: number | null) => `${(y || 0) * 100}%`; -const formatTooltipValue = (c: Coordinate) => asPercent(c.y || 0, 1); - -export function CPUUsageChart({ data, hoverXHandlers }: Props) { - return ( - - - - {i18n.translate( - 'xpack.apm.serviceDetails.metrics.cpuUsageChartTitle', - { - defaultMessage: 'CPU usage' - } - )} - - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/MemoryUsageChart.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/MemoryUsageChart.tsx deleted file mode 100644 index f452cadef20d8..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/MemoryUsageChart.tsx +++ /dev/null @@ -1,48 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { Coordinate } from '../../../../typings/timeseries'; -import { MemoryMetricSeries } from '../../../selectors/chartSelectors'; -import { asPercent } from '../../../utils/formatters'; -// @ts-ignore -import CustomPlot from '../../shared/charts/CustomPlot'; -import { HoverXHandlers } from '../../shared/charts/SyncChartGroup'; - -interface Props { - data: MemoryMetricSeries; - hoverXHandlers: HoverXHandlers; -} - -const tickFormatY = (y: number | null) => `${(y || 0) * 100}%`; -const formatTooltipValue = (c: Coordinate) => asPercent(c.y || 0, 1); - -export function MemoryUsageChart({ data, hoverXHandlers }: Props) { - return ( - - - - {i18n.translate( - 'xpack.apm.serviceDetails.metrics.memoryUsageChartTitle', - { - defaultMessage: 'Memory usage' - } - )} - - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/MetricsChart.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/MetricsChart.tsx new file mode 100644 index 0000000000000..363cfa0ccf65c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/MetricsChart.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiTitle } from '@elastic/eui'; +import React from 'react'; +import { GenericMetricsChart } from '../../../../server/lib/metrics/transform_metrics_chart'; +// @ts-ignore +import CustomPlot from '../../shared/charts/CustomPlot'; +import { HoverXHandlers } from '../../shared/charts/SyncChartGroup'; +import { + asDynamicBytes, + asPercent, + getFixedByteFormatter, + asDecimal +} from '../../../utils/formatters'; +import { Coordinate } from '../../../../typings/timeseries'; + +interface Props { + chart: GenericMetricsChart; + hoverXHandlers: HoverXHandlers; +} + +export function MetricsChart({ chart, hoverXHandlers }: Props) { + const formatYValue = getYTickFormatter(chart); + const formatTooltip = getTooltipFormatter(chart); + + const transformedSeries = chart.series.map(series => ({ + ...series, + legendValue: formatYValue(series.overallValue) + })); + + return ( + + + {chart.title} + + + + ); +} + +function getYTickFormatter(chart: GenericMetricsChart) { + switch (chart.yUnit) { + case 'bytes': { + const max = Math.max( + ...chart.series.flatMap(series => + series.data.map(coord => coord.y || 0) + ) + ); + return getFixedByteFormatter(max); + } + case 'percent': { + return (y: number | null) => asPercent(y || 0, 1); + } + default: { + return (y: number | null) => (y === null ? y : asDecimal(y)); + } + } +} + +function getTooltipFormatter({ yUnit }: GenericMetricsChart) { + switch (yUnit) { + case 'bytes': { + return (c: Coordinate) => asDynamicBytes(c.y); + } + case 'percent': { + return (c: Coordinate) => asPercent(c.y || 0, 1); + } + default: { + return (c: Coordinate) => (c.y === null ? c.y : asDecimal(c.y)); + } + } +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index d3c1e59bce745..45775d4e51cac 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -17,12 +17,14 @@ interface Props { transactionTypes: string[]; urlParams: IUrlParams; isRumAgent?: boolean; + agentName: string; } export function ServiceDetailTabs({ transactionTypes, urlParams, - isRumAgent + isRumAgent, + agentName }: Props) { const location = useLocation(); const { serviceName } = urlParams; @@ -56,7 +58,7 @@ export function ServiceDetailTabs({ defaultMessage: 'Metrics' }), path: `/${serviceName}/metrics`, - render: () => + render: () => }; const tabs = isRumAgent ? [transactionsTab, errorsTab] diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceMetrics.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceMetrics.tsx index 4d892be9b5d9d..53bae13581ba0 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceMetrics.tsx @@ -4,101 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiFlexGrid, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSpacer -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Location } from 'history'; +import { EuiFlexGrid, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; import React from 'react'; -import { useFetcher } from '../../../hooks/useFetcher'; import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; -import { useTransactionOverviewCharts } from '../../../hooks/useTransactionOverviewCharts'; -import { loadErrorDistribution } from '../../../services/rest/apm/error_groups'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; import { SyncChartGroup } from '../../shared/charts/SyncChartGroup'; -import { TransactionCharts } from '../../shared/charts/TransactionCharts'; -import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; -import { CPUUsageChart } from './CPUUsageChart'; -import { MemoryUsageChart } from './MemoryUsageChart'; +import { MetricsChart } from './MetricsChart'; interface ServiceMetricsProps { urlParams: IUrlParams; - location: Location; + agentName: string; } -export function ServiceMetrics({ urlParams, location }: ServiceMetricsProps) { - const { serviceName, start, end, kuery } = urlParams; - const { data: errorDistributionData } = useFetcher( - () => { - if (serviceName && start && end) { - return loadErrorDistribution({ serviceName, start, end, kuery }); - } - }, - [serviceName, start, end, kuery] - ); - - const { data: transactionOverviewChartsData } = useTransactionOverviewCharts( - urlParams - ); - - const { data: serviceMetricChartData } = useServiceMetricCharts(urlParams); - - if (!errorDistributionData) { - return null; - } - +export function ServiceMetrics({ urlParams, agentName }: ServiceMetricsProps) { + const { data } = useServiceMetricCharts(urlParams, agentName); return ( - - - - - - - - - - - - - - ( - - - - - - - - - - - + + {data.charts.map(chart => ( + + + + + + ))} )} /> diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx index 7f93c8078bbd0..5ca0cae04d1ed 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx @@ -30,7 +30,7 @@ export function ServiceDetails() { return null; } - const isRumAgent = isRumAgentName(serviceDetailsData.agentName || ''); + const isRumAgent = isRumAgentName(serviceDetailsData.agentName); return ( @@ -56,6 +56,7 @@ export function ServiceDetails() { urlParams={urlParams} transactionTypes={serviceDetailsData.types} isRumAgent={isRumAgent} + agentName={serviceDetailsData.agentName} /> ); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx index 48223aceedd7d..7febbcd636613 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx @@ -109,7 +109,7 @@ export const Transaction: React.SFC = ({ - +
{i18n.translate( 'xpack.apm.transactionDetails.transactionSampleTitle', @@ -134,8 +134,6 @@ export const Transaction: React.SFC = ({ - - - + - + {!transaction ? ( - + diff --git a/x-pack/plugins/apm/public/components/shared/Icons.tsx b/x-pack/plugins/apm/public/components/shared/Icons.tsx index 28fd5ab980685..79b34b2818aac 100644 --- a/x-pack/plugins/apm/public/components/shared/Icons.tsx +++ b/x-pack/plugins/apm/public/components/shared/Icons.tsx @@ -6,17 +6,15 @@ import { EuiIcon } from '@elastic/eui'; import React from 'react'; -import { units } from '../../style/variables'; export function Ellipsis({ horizontal }: { horizontal: boolean }) { return ( ); } diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js index 28773913c03e2..260b53abca76b 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js @@ -174,7 +174,7 @@ export class Typeahead extends Component { 'Search transactions and errors… (E.g. {queryExample})', values: { queryExample: - 'transaction.duration.us > 300000 AND context.response.status_code >= 400' + 'transaction.duration.us > 300000 AND http.response.status_code >= 400' } } )} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js index 615de4c170065..6687ce5f2ed1d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js @@ -87,7 +87,7 @@ describe('Histogram', () => { const tooltips = wrapper.find('Tooltip'); expect(tooltips.length).toBe(1); - expect(tooltips.prop('header')).toBe('811 - 869 ms'); + expect(tooltips.prop('header')).toBe('811 - 927 ms'); expect(tooltips.prop('tooltipPoints')).toEqual([ { value: '49.0 occurrences' } ]); @@ -102,8 +102,9 @@ describe('Histogram', () => { transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192' }, style: { cursor: 'pointer' }, - x: 869010, + xCenter: 869010, x0: 811076, + x: 926944, y: 49 } }); @@ -129,8 +130,9 @@ describe('Histogram', () => { transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192' }, style: { cursor: 'pointer' }, - x: 869010, + xCenter: 869010, x0: 811076, + x: 926944, y: 49 }); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap index 42be9a9325072..305acafcf5597 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap @@ -1461,7 +1461,7 @@ exports[`Histogram when hovering over a non-empty bucket should have correct mar
- 811 - 869 ms + 811 - 927 ms
)} @@ -206,13 +207,13 @@ export class HistogramInner extends PureComponent { nodes={this.props.buckets.map(bucket => { return { ...bucket, - x: (bucket.x0 + bucket.x) / 2 + xCenter: (bucket.x0 + bucket.x) / 2 }; })} onClick={this.onClick} onHover={this.onHover} onBlur={this.onBlur} - x={d => x(d.x)} + x={d => x(d.xCenter)} y={() => 1} /> diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 08139af0487c8..9df22aef26e2b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -139,7 +139,7 @@ export class TransactionCharts extends Component { return ( ( - + diff --git a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts index 2b38a404ebe10..dbf6052fa38bd 100644 --- a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts +++ b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts @@ -4,53 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useMemo } from 'react'; -import { MetricsChartAPIResponse } from '../../server/lib/metrics/get_all_metrics_chart_data'; -import { MemoryChartAPIResponse } from '../../server/lib/metrics/get_memory_chart_data'; -import { loadMetricsChartDataForService } from '../services/rest/apm/metrics'; -import { getCPUSeries, getMemorySeries } from '../selectors/chartSelectors'; +import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_metrics_chart_data_by_agent'; +import { loadMetricsChartData } from '../services/rest/apm/metrics'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; -const memory: MemoryChartAPIResponse = { - series: { - memoryUsedAvg: [], - memoryUsedMax: [] - }, - overallValues: { - memoryUsedAvg: null, - memoryUsedMax: null - }, - totalHits: 0 +const INITIAL_DATA: MetricsChartsByAgentAPIResponse = { + charts: [] }; -const INITIAL_DATA: MetricsChartAPIResponse = { - memory, - cpu: { - series: { - systemCPUAverage: [], - systemCPUMax: [], - processCPUAverage: [], - processCPUMax: [] - }, - overallValues: { - systemCPUAverage: null, - systemCPUMax: null, - processCPUAverage: null, - processCPUMax: null - }, - totalHits: 0 - } -}; - -export function useServiceMetricCharts(urlParams: IUrlParams) { +export function useServiceMetricCharts( + urlParams: IUrlParams, + agentName: string +) { const { serviceName, start, end, kuery } = urlParams; - const { data = INITIAL_DATA, error, status } = useFetcher( + const { data = INITIAL_DATA, error, status } = useFetcher< + MetricsChartsByAgentAPIResponse + >( () => { if (serviceName && start && end) { - return loadMetricsChartDataForService({ + return loadMetricsChartData({ serviceName, + agentName, start, end, kuery @@ -60,16 +36,8 @@ export function useServiceMetricCharts(urlParams: IUrlParams) { [serviceName, start, end, kuery] ); - const memoizedData = useMemo( - () => ({ - memory: getMemorySeries(urlParams, data.memory), - cpu: getCPUSeries(data.cpu) - }), - [data] - ); - return { - data: memoizedData, + data, status, error }; diff --git a/x-pack/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chartSelectors.ts index 6f9ff73dbaae6..b69fe68522aad 100644 --- a/x-pack/plugins/apm/public/selectors/chartSelectors.ts +++ b/x-pack/plugins/apm/public/selectors/chartSelectors.ts @@ -10,12 +10,11 @@ import d3 from 'd3'; import { difference, memoize, zipObject } from 'lodash'; import mean from 'lodash.mean'; import { rgba } from 'polished'; -import { MetricsChartAPIResponse } from '../../server/lib/metrics/get_all_metrics_chart_data'; import { TimeSeriesAPIResponse } from '../../server/lib/transactions/charts'; import { ApmTimeSeriesResponse } from '../../server/lib/transactions/charts/get_timeseries_data/transform'; import { StringMap } from '../../typings/common'; import { Coordinate, RectCoordinate } from '../../typings/timeseries'; -import { asDecimal, asMillis, asPercent, tpmUnit } from '../utils/formatters'; +import { asDecimal, asMillis, tpmUnit } from '../utils/formatters'; import { IUrlParams } from '../context/UrlParamsContext/types'; export const getEmptySerie = memoize( @@ -92,96 +91,6 @@ export function getTransactionCharts( }; } -export type MemoryMetricSeries = ReturnType; - -export function getMemorySeries( - { start, end }: IUrlParams, - memoryChartResponse: MetricsChartAPIResponse['memory'] -) { - const { series, overallValues, totalHits } = memoryChartResponse; - const seriesList = - totalHits === 0 - ? getEmptySerie(start, end) - : [ - { - title: i18n.translate( - 'xpack.apm.chart.memorySeries.systemMaxLabel', - { - defaultMessage: 'System max' - } - ), - data: series.memoryUsedMax, - type: 'linemark', - color: theme.euiColorVis1, - legendValue: asPercent(overallValues.memoryUsedMax || 0, 1) - }, - { - title: i18n.translate( - 'xpack.apm.chart.memorySeries.systemAverageLabel', - { - defaultMessage: 'System average' - } - ), - data: series.memoryUsedAvg, - type: 'linemark', - color: theme.euiColorVis0, - legendValue: asPercent(overallValues.memoryUsedAvg || 0, 1) - } - ]; - - return { - totalHits: memoryChartResponse.totalHits, - series: seriesList - }; -} - -export type CPUMetricSeries = ReturnType; - -export function getCPUSeries(CPUChartResponse: MetricsChartAPIResponse['cpu']) { - const { series, overallValues } = CPUChartResponse; - - const seriesList: TimeSerie[] = [ - { - title: i18n.translate('xpack.apm.chart.cpuSeries.systemMaxLabel', { - defaultMessage: 'System max' - }), - data: series.systemCPUMax, - type: 'linemark', - color: theme.euiColorVis1, - legendValue: asPercent(overallValues.systemCPUMax || 0, 1) - }, - { - title: i18n.translate('xpack.apm.chart.cpuSeries.systemAverageLabel', { - defaultMessage: 'System average' - }), - data: series.systemCPUAverage, - type: 'linemark', - color: theme.euiColorVis0, - legendValue: asPercent(overallValues.systemCPUAverage || 0, 1) - }, - { - title: i18n.translate('xpack.apm.chart.cpuSeries.processMaxLabel', { - defaultMessage: 'Process max' - }), - data: series.processCPUMax, - type: 'linemark', - color: theme.euiColorVis7, - legendValue: asPercent(overallValues.processCPUMax || 0, 1) - }, - { - title: i18n.translate('xpack.apm.chart.cpuSeries.processAverageLabel', { - defaultMessage: 'Process average' - }), - data: series.processCPUAverage, - type: 'linemark', - color: theme.euiColorVis5, - legendValue: asPercent(overallValues.processCPUAverage || 0, 1) - } - ]; - - return { totalHits: CPUChartResponse.totalHits, series: seriesList }; -} - interface TimeSerie { title: string; titleShort?: string; diff --git a/x-pack/plugins/apm/public/services/rest/apm/metrics.ts b/x-pack/plugins/apm/public/services/rest/apm/metrics.ts index 33407a963d676..e33e4a6802875 100644 --- a/x-pack/plugins/apm/public/services/rest/apm/metrics.ts +++ b/x-pack/plugins/apm/public/services/rest/apm/metrics.ts @@ -4,27 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MetricsChartAPIResponse } from '../../../../server/lib/metrics/get_all_metrics_chart_data'; +import { MetricsChartsByAgentAPIResponse } from '../../../../server/lib/metrics/get_metrics_chart_data_by_agent'; import { callApi } from '../callApi'; import { getEncodedEsQuery } from './apm'; -export async function loadMetricsChartDataForService({ +export async function loadMetricsChartData({ serviceName, + agentName, start, end, kuery }: { serviceName: string; + agentName: string; start: string; end: string; kuery: string | undefined; }) { - return callApi({ + return callApi({ pathname: `/api/apm/services/${serviceName}/metrics/charts`, query: { start, end, - esFilterQuery: await getEncodedEsQuery(kuery) + esFilterQuery: await getEncodedEsQuery(kuery), + agentName } }); } diff --git a/x-pack/plugins/apm/public/utils/__test__/formatters.test.ts b/x-pack/plugins/apm/public/utils/__test__/formatters.test.ts index 43aa096c38102..678ab9009edd7 100644 --- a/x-pack/plugins/apm/public/utils/__test__/formatters.test.ts +++ b/x-pack/plugins/apm/public/utils/__test__/formatters.test.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { asPercent, asTime } from '../formatters'; +import { + asPercent, + asTime, + getFixedByteFormatter, + asDynamicBytes +} from '../formatters'; describe('formatters', () => { describe('asTime', () => { @@ -50,4 +55,77 @@ describe('formatters', () => { expect(asPercent(NaN, 10000, 'n/a')).toEqual('n/a'); }); }); + + describe('byte formatting', () => { + const bytes = 10; + const kb = 1000 + 1; + const mb = 1e6 + 1; + const gb = 1e9 + 1; + const tb = 1e12 + 1; + + test('dynamic', () => { + expect(asDynamicBytes(bytes)).toEqual('10.0 B'); + expect(asDynamicBytes(kb)).toEqual('1.0 KB'); + expect(asDynamicBytes(mb)).toEqual('1.0 MB'); + expect(asDynamicBytes(gb)).toEqual('1.0 GB'); + expect(asDynamicBytes(tb)).toEqual('1.0 TB'); + expect(asDynamicBytes(null)).toEqual(''); + expect(asDynamicBytes(NaN)).toEqual(''); + }); + + describe('fixed', () => { + test('in bytes', () => { + const formatInBytes = getFixedByteFormatter(bytes); + expect(formatInBytes(bytes)).toEqual('10.0 B'); + expect(formatInBytes(kb)).toEqual('1,001.0 B'); + expect(formatInBytes(mb)).toEqual('1,000,001.0 B'); + expect(formatInBytes(gb)).toEqual('1,000,000,001.0 B'); + expect(formatInBytes(tb)).toEqual('1,000,000,000,001.0 B'); + expect(formatInBytes(null)).toEqual(''); + expect(formatInBytes(NaN)).toEqual(''); + }); + + test('in kb', () => { + const formatInKB = getFixedByteFormatter(kb); + expect(formatInKB(bytes)).toEqual('0.0 KB'); + expect(formatInKB(kb)).toEqual('1.0 KB'); + expect(formatInKB(mb)).toEqual('1,000.0 KB'); + expect(formatInKB(gb)).toEqual('1,000,000.0 KB'); + expect(formatInKB(tb)).toEqual('1,000,000,000.0 KB'); + }); + + test('in mb', () => { + const formatInMB = getFixedByteFormatter(mb); + expect(formatInMB(bytes)).toEqual('0.0 MB'); + expect(formatInMB(kb)).toEqual('0.0 MB'); + expect(formatInMB(mb)).toEqual('1.0 MB'); + expect(formatInMB(gb)).toEqual('1,000.0 MB'); + expect(formatInMB(tb)).toEqual('1,000,000.0 MB'); + expect(formatInMB(null)).toEqual(''); + expect(formatInMB(NaN)).toEqual(''); + }); + + test('in gb', () => { + const formatInGB = getFixedByteFormatter(gb); + expect(formatInGB(bytes)).toEqual('1e-8 GB'); + expect(formatInGB(kb)).toEqual('0.0 GB'); + expect(formatInGB(mb)).toEqual('0.0 GB'); + expect(formatInGB(gb)).toEqual('1.0 GB'); + expect(formatInGB(tb)).toEqual('1,000.0 GB'); + expect(formatInGB(null)).toEqual(''); + expect(formatInGB(NaN)).toEqual(''); + }); + + test('in tb', () => { + const formatInTB = getFixedByteFormatter(tb); + expect(formatInTB(bytes)).toEqual('1e-11 TB'); + expect(formatInTB(kb)).toEqual('1.001e-9 TB'); + expect(formatInTB(mb)).toEqual('0.0 TB'); + expect(formatInTB(gb)).toEqual('0.0 TB'); + expect(formatInTB(tb)).toEqual('1.0 TB'); + expect(formatInTB(null)).toEqual(''); + expect(formatInTB(NaN)).toEqual(''); + }); + }); + }); }); diff --git a/x-pack/plugins/apm/public/utils/formatters.ts b/x-pack/plugins/apm/public/utils/formatters.ts index 820a58c1b4b7a..f1d3053fcf12e 100644 --- a/x-pack/plugins/apm/public/utils/formatters.ts +++ b/x-pack/plugins/apm/public/utils/formatters.ts @@ -141,3 +141,71 @@ export function asPercent( const decimal = numerator / denominator; return numeral(decimal).format('0.0%'); } + +type ByteFormatter = (value: number | null) => string; + +function asKilobytes(value: number | null) { + if (value === null || isNaN(value)) { + return ''; + } + return `${asDecimal(value / 1000)} KB`; +} + +function asMegabytes(value: number | null) { + if (value === null || isNaN(value)) { + return ''; + } + return `${asDecimal(value / 1e6)} MB`; +} + +function asGigabytes(value: number | null) { + if (value === null || isNaN(value)) { + return ''; + } + return `${asDecimal(value / 1e9)} GB`; +} + +function asTerabytes(value: number | null) { + if (value === null || isNaN(value)) { + return ''; + } + return `${asDecimal(value / 1e12)} TB`; +} + +export function asBytes(value: number | null) { + if (value === null || isNaN(value)) { + return ''; + } + return `${asDecimal(value)} B`; +} + +export function asDynamicBytes(value: number | null) { + if (value === null || isNaN(value)) { + return ''; + } + return unmemoizedFixedByteFormatter(value)(value); +} + +type GetByteFormatter = (max: number) => ByteFormatter; + +const unmemoizedFixedByteFormatter: GetByteFormatter = max => { + if (max > 1e12) { + return asTerabytes; + } + + if (max > 1e9) { + return asGigabytes; + } + + if (max > 1e6) { + return asMegabytes; + } + + if (max > 1000) { + return asKilobytes; + } + + return asBytes; +}; + +export const getFixedByteFormatter = memoize(unmemoizedFixedByteFormatter); diff --git a/x-pack/plugins/apm/server/lib/helpers/metrics.ts b/x-pack/plugins/apm/server/lib/helpers/metrics.ts new file mode 100644 index 0000000000000..df2d6dcf523bf --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/metrics.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getBucketSize } from './get_bucket_size'; + +export function getMetricsDateHistogramParams(start: number, end: number) { + const { bucketSize } = getBucketSize(start, end, 'auto'); + return { + field: '@timestamp', + + // ensure minimum bucket size of 30s since this is the default resolution for metric data + interval: `${Math.max(bucketSize, 30)}s`, + + min_doc_count: 0, + extended_bounds: { min: start, max: end } + }; +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts new file mode 100644 index 0000000000000..f55d3320c7f4e --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/default.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Setup } from '../../helpers/setup_request'; +import { getCPUChartData } from './shared/cpu'; +import { getMemoryChartData } from './shared/memory'; + +export async function getDefaultMetricsCharts( + setup: Setup, + serviceName: string +) { + const charts = await Promise.all([ + getCPUChartData(setup, serviceName), + getMemoryChartData(setup, serviceName) + ]); + + return { charts }; +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/fetcher.ts new file mode 100644 index 0000000000000..8d3a06a3c5a37 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/fetcher.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ESFilter } from 'elasticsearch'; +import { + SERVICE_AGENT_NAME, + PROCESSOR_EVENT, + SERVICE_NAME, + METRIC_JAVA_HEAP_MEMORY_MAX, + METRIC_JAVA_HEAP_MEMORY_COMMITTED, + METRIC_JAVA_HEAP_MEMORY_USED +} from '../../../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../../../helpers/setup_request'; +import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types'; +import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; +import { rangeFilter } from '../../../../helpers/range_filter'; + +export interface HeapMemoryMetrics extends MetricSeriesKeys { + heapMemoryMax: AggValue; + heapMemoryCommitted: AggValue; + heapMemoryUsed: AggValue; +} + +export async function fetch(setup: Setup, serviceName: string) { + const { start, end, esFilterQuery, client, config } = setup; + const filters: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'metric' } }, + { term: { [SERVICE_AGENT_NAME]: 'java' } }, + { + range: rangeFilter(start, end) + } + ]; + + if (esFilterQuery) { + filters.push(esFilterQuery); + } + + const aggs = { + heapMemoryMax: { avg: { field: METRIC_JAVA_HEAP_MEMORY_MAX } }, + heapMemoryCommitted: { + avg: { field: METRIC_JAVA_HEAP_MEMORY_COMMITTED } + }, + heapMemoryUsed: { avg: { field: METRIC_JAVA_HEAP_MEMORY_USED } } + }; + + const params = { + index: config.get('apm_oss.metricsIndices'), + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: { + timeseriesData: { + date_histogram: getMetricsDateHistogramParams(start, end), + aggs + }, + ...aggs + } + } + }; + + return client>('search', params); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts new file mode 100644 index 0000000000000..eb6ffdb35561f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { Setup } from '../../../../helpers/setup_request'; +import { fetch, HeapMemoryMetrics } from './fetcher'; +import { ChartBase } from '../../../types'; +import { transformDataToMetricsChart } from '../../../transform_metrics_chart'; + +// TODO: i18n for titles + +const chartBase: ChartBase = { + title: i18n.translate('xpack.apm.agentMetrics.java.heapMemoryChartTitle', { + defaultMessage: 'Heap Memory' + }), + key: 'heap_memory_area_chart', + type: 'area', + yUnit: 'bytes', + series: { + heapMemoryUsed: { + title: i18n.translate( + 'xpack.apm.agentMetrics.java.heapMemorySeriesUsed', + { + defaultMessage: 'Used' + } + ), + color: theme.euiColorVis0 + }, + heapMemoryCommitted: { + title: i18n.translate( + 'xpack.apm.agentMetrics.java.heapMemorySeriesCommitted', + { + defaultMessage: 'Committed' + } + ), + color: theme.euiColorVis1 + }, + heapMemoryMax: { + title: i18n.translate( + 'xpack.apm.agentMetrics.java.nonHeapMemorySeriesMax', + { + defaultMessage: 'Max' + } + ), + color: theme.euiColorVis2 + } + } +}; + +export async function getHeapMemoryChart(setup: Setup, serviceName: string) { + const result = await fetch(setup, serviceName); + return transformDataToMetricsChart(result, chartBase); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts new file mode 100644 index 0000000000000..6aacf24102694 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getHeapMemoryChart } from './heap_memory'; +import { Setup } from '../../../helpers/setup_request'; +import { getNonHeapMemoryChart } from './non_heap_memory'; +import { getThreadCountChart } from './thread_count'; + +export async function getJavaMetricsCharts(setup: Setup, serviceName: string) { + const charts = await Promise.all([ + getHeapMemoryChart(setup, serviceName), + getNonHeapMemoryChart(setup, serviceName), + getThreadCountChart(setup, serviceName) + ]); + + return { charts }; +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/fetcher.ts new file mode 100644 index 0000000000000..f719993c819f3 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/fetcher.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ESFilter } from 'elasticsearch'; +import { + SERVICE_AGENT_NAME, + PROCESSOR_EVENT, + SERVICE_NAME, + METRIC_JAVA_NON_HEAP_MEMORY_MAX, + METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED, + METRIC_JAVA_NON_HEAP_MEMORY_USED +} from '../../../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../../../helpers/setup_request'; +import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types'; +import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; +import { rangeFilter } from '../../../../helpers/range_filter'; + +export interface NonHeapMemoryMetrics extends MetricSeriesKeys { + nonHeapMemoryCommitted: AggValue; + nonHeapMemoryUsed: AggValue; +} + +export async function fetch(setup: Setup, serviceName: string) { + const { start, end, esFilterQuery, client, config } = setup; + const filters: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'metric' } }, + { term: { [SERVICE_AGENT_NAME]: 'java' } }, + { + range: rangeFilter(start, end) + } + ]; + + if (esFilterQuery) { + filters.push(esFilterQuery); + } + + const aggs = { + nonHeapMemoryMax: { avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_MAX } }, + nonHeapMemoryCommitted: { + avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED } + }, + nonHeapMemoryUsed: { + avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_USED } + } + }; + + const params = { + index: config.get('apm_oss.metricsIndices'), + body: { + size: 0, + query: { + bool: { + filter: filters + } + }, + aggs: { + timeseriesData: { + date_histogram: getMetricsDateHistogramParams(start, end), + aggs + }, + ...aggs + } + } + }; + + return client>('search', params); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts new file mode 100644 index 0000000000000..2ddbd5f23e19c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { Setup } from '../../../../helpers/setup_request'; +import { fetch, NonHeapMemoryMetrics } from './fetcher'; +import { ChartBase } from '../../../types'; +import { transformDataToMetricsChart } from '../../../transform_metrics_chart'; + +const chartBase: ChartBase = { + title: i18n.translate('xpack.apm.agentMetrics.java.nonHeapMemoryChartTitle', { + defaultMessage: 'Non-Heap Memory' + }), + key: 'non_heap_memory_area_chart', + type: 'area', + yUnit: 'bytes', + series: { + nonHeapMemoryUsed: { + title: i18n.translate( + 'xpack.apm.agentMetrics.java.nonHeapMemorySeriesUsed', + { + defaultMessage: 'Used' + } + ), + color: theme.euiColorVis0 + }, + nonHeapMemoryCommitted: { + title: i18n.translate( + 'xpack.apm.agentMetrics.java.nonHeapMemorySeriesCommitted', + { + defaultMessage: 'Committed' + } + ), + color: theme.euiColorVis1 + } + } +}; + +export async function getNonHeapMemoryChart(setup: Setup, serviceName: string) { + const result = await fetch(setup, serviceName); + return transformDataToMetricsChart(result, chartBase); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/fetcher.ts new file mode 100644 index 0000000000000..328864025d956 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/fetcher.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ESFilter } from 'elasticsearch'; +import { + SERVICE_AGENT_NAME, + PROCESSOR_EVENT, + SERVICE_NAME, + METRIC_JAVA_THREAD_COUNT +} from '../../../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../../../helpers/setup_request'; +import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types'; +import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; +import { rangeFilter } from '../../../../helpers/range_filter'; + +export interface ThreadCountMetrics extends MetricSeriesKeys { + threadCount: AggValue; +} + +export async function fetch(setup: Setup, serviceName: string) { + const { start, end, esFilterQuery, client, config } = setup; + const filters: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'metric' } }, + { term: { [SERVICE_AGENT_NAME]: 'java' } }, + { + range: rangeFilter(start, end) + } + ]; + + if (esFilterQuery) { + filters.push(esFilterQuery); + } + + const aggs = { + threadCount: { avg: { field: METRIC_JAVA_THREAD_COUNT } } + }; + + const params = { + index: config.get('apm_oss.metricsIndices'), + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: { + timeseriesData: { + date_histogram: getMetricsDateHistogramParams(start, end), + aggs + }, + ...aggs + } + } + }; + + return client>('search', params); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts new file mode 100644 index 0000000000000..9b1e441065361 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { Setup } from '../../../../helpers/setup_request'; +import { fetch, ThreadCountMetrics } from './fetcher'; +import { ChartBase } from '../../../types'; +import { transformDataToMetricsChart } from '../../../transform_metrics_chart'; + +const chartBase: ChartBase = { + title: i18n.translate('xpack.apm.agentMetrics.java.threadCountChartTitle', { + defaultMessage: 'Thread Count' + }), + key: 'thread_count_line_chart', + type: 'linemark', + yUnit: 'number', + series: { + threadCount: { + title: i18n.translate('xpack.apm.agentMetrics.java.threadCount', { + defaultMessage: 'Count' + }), + color: theme.euiColorVis0 + } + } +}; + +export async function getThreadCountChart(setup: Setup, serviceName: string) { + const result = await fetch(setup, serviceName); + return transformDataToMetricsChart(result, chartBase); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/fetcher.ts new file mode 100644 index 0000000000000..e45c69c5f8760 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/fetcher.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESFilter } from 'elasticsearch'; +import { + METRIC_PROCESS_CPU_PERCENT, + METRIC_SYSTEM_CPU_PERCENT, + PROCESSOR_EVENT, + SERVICE_NAME +} from '../../../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../../../helpers/setup_request'; +import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types'; +import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; +import { rangeFilter } from '../../../../helpers/range_filter'; + +export interface CPUMetrics extends MetricSeriesKeys { + systemCPUAverage: AggValue; + systemCPUMax: AggValue; + processCPUAverage: AggValue; + processCPUMax: AggValue; +} + +export async function fetch(setup: Setup, serviceName: string) { + const { start, end, esFilterQuery, client, config } = setup; + const filters: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'metric' } }, + { + range: rangeFilter(start, end) + } + ]; + + if (esFilterQuery) { + filters.push(esFilterQuery); + } + + const aggs = { + systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } }, + systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } }, + processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } }, + processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } } + }; + + const params = { + index: config.get('apm_oss.metricsIndices'), + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: { + timeseriesData: { + date_histogram: getMetricsDateHistogramParams(start, end), + aggs + }, + ...aggs + } + } + }; + + return client>('search', params); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts new file mode 100644 index 0000000000000..f7fe92d578e93 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { Setup } from '../../../../helpers/setup_request'; +import { fetch, CPUMetrics } from './fetcher'; +import { ChartBase } from '../../../types'; +import { transformDataToMetricsChart } from '../../../transform_metrics_chart'; + +const chartBase: ChartBase = { + title: i18n.translate('xpack.apm.serviceDetails.metrics.cpuUsageChartTitle', { + defaultMessage: 'CPU usage' + }), + key: 'cpu_usage_chart', + type: 'linemark', + yUnit: 'percent', + series: { + systemCPUMax: { + title: i18n.translate('xpack.apm.chart.cpuSeries.systemMaxLabel', { + defaultMessage: 'System max' + }), + color: theme.euiColorVis1 + }, + systemCPUAverage: { + title: i18n.translate('xpack.apm.chart.cpuSeries.systemAverageLabel', { + defaultMessage: 'System average' + }), + color: theme.euiColorVis0 + }, + processCPUMax: { + title: i18n.translate('xpack.apm.chart.cpuSeries.processMaxLabel', { + defaultMessage: 'Process max' + }), + color: theme.euiColorVis7 + }, + processCPUAverage: { + title: i18n.translate('xpack.apm.chart.cpuSeries.processAverageLabel', { + defaultMessage: 'Process average' + }), + color: theme.euiColorVis5 + } + } +}; + +export async function getCPUChartData(setup: Setup, serviceName: string) { + const result = await fetch(setup, serviceName); + return transformDataToMetricsChart(result, chartBase); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/fetcher.ts new file mode 100644 index 0000000000000..a4d378b6ebaca --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/fetcher.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESFilter } from 'elasticsearch'; +import { + PROCESSOR_EVENT, + SERVICE_NAME, + METRIC_SYSTEM_FREE_MEMORY, + METRIC_SYSTEM_TOTAL_MEMORY +} from '../../../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../../../helpers/setup_request'; +import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types'; +import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; +import { rangeFilter } from '../../../../helpers/range_filter'; + +export interface MemoryMetrics extends MetricSeriesKeys { + memoryUsedAvg: AggValue; + memoryUsedMax: AggValue; +} + +const percentUsedScript = { + lang: 'expression', + source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']` +}; + +export async function fetch(setup: Setup, serviceName: string) { + const { start, end, esFilterQuery, client, config } = setup; + const filters: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'metric' } }, + { + range: rangeFilter(start, end) + } + ]; + + if (esFilterQuery) { + filters.push(esFilterQuery); + } + + const aggs = { + memoryUsedAvg: { avg: { script: percentUsedScript } }, + memoryUsedMax: { max: { script: percentUsedScript } } + }; + + const params = { + index: config.get('apm_oss.metricsIndices'), + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: { + timeseriesData: { + date_histogram: getMetricsDateHistogramParams(start, end), + aggs + }, + ...aggs + } + } + }; + + return client>('search', params); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts new file mode 100644 index 0000000000000..f5976972e6d5e --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { Setup } from '../../../../helpers/setup_request'; +import { fetch, MemoryMetrics } from './fetcher'; +import { ChartBase } from '../../../types'; +import { transformDataToMetricsChart } from '../../../transform_metrics_chart'; + +const chartBase: ChartBase = { + title: i18n.translate( + 'xpack.apm.serviceDetails.metrics.memoryUsageChartTitle', + { + defaultMessage: 'Memory usage' + } + ), + key: 'memory_usage_chart', + type: 'linemark', + yUnit: 'percent', + series: { + memoryUsedMax: { + title: i18n.translate('xpack.apm.chart.memorySeries.systemMaxLabel', { + defaultMessage: 'System max' + }) + }, + memoryUsedAvg: { + title: i18n.translate('xpack.apm.chart.memorySeries.systemAverageLabel', { + defaultMessage: 'System average' + }) + } + } +}; + +export async function getMemoryChartData(setup: Setup, serviceName: string) { + const result = await fetch(setup, serviceName); + return transformDataToMetricsChart(result, chartBase); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/get_all_metrics_chart_data.ts b/x-pack/plugins/apm/server/lib/metrics/get_all_metrics_chart_data.ts deleted file mode 100644 index b16c3e16d330c..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_all_metrics_chart_data.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PromiseReturnType } from '../../../typings/common'; -import { getCPUChartData } from './get_cpu_chart_data'; -import { getMemoryChartData } from './get_memory_chart_data'; -import { MetricsRequestArgs } from './query_types'; - -export type MetricsChartAPIResponse = PromiseReturnType< - typeof getAllMetricsChartData ->; -export async function getAllMetricsChartData(args: MetricsRequestArgs) { - const [memoryChartData, cpuChartData] = await Promise.all([ - getMemoryChartData(args), - getCPUChartData(args) - ]); - return { - memory: memoryChartData, - cpu: cpuChartData - }; -} diff --git a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap deleted file mode 100644 index 0c5b0e346f8ff..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap +++ /dev/null @@ -1,100 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CPU chart data fetcher should fetch aggregations 1`] = ` -Array [ - Array [ - "search", - Object { - "body": Object { - "aggs": Object { - "processCPUAverage": Object { - "avg": Object { - "field": "system.process.cpu.total.norm.pct", - }, - }, - "processCPUMax": Object { - "max": Object { - "field": "system.process.cpu.total.norm.pct", - }, - }, - "systemCPUAverage": Object { - "avg": Object { - "field": "system.cpu.total.norm.pct", - }, - }, - "systemCPUMax": Object { - "max": Object { - "field": "system.cpu.total.norm.pct", - }, - }, - "timeseriesData": Object { - "aggs": Object { - "processCPUAverage": Object { - "avg": Object { - "field": "system.process.cpu.total.norm.pct", - }, - }, - "processCPUMax": Object { - "max": Object { - "field": "system.process.cpu.total.norm.pct", - }, - }, - "systemCPUAverage": Object { - "avg": Object { - "field": "system.cpu.total.norm.pct", - }, - }, - "systemCPUMax": Object { - "max": Object { - "field": "system.cpu.total.norm.pct", - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 200, - "min": 100, - }, - "field": "@timestamp", - "interval": "30s", - "min_doc_count": 0, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "service.name": "test-service", - }, - }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 100, - "lte": 200, - }, - }, - }, - Object { - "term": Object { - "field": "test.esfilter.query", - }, - }, - ], - }, - }, - "size": 0, - }, - "index": undefined, - }, - ], -] -`; diff --git a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/__snapshots__/transformer.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/__snapshots__/transformer.test.ts.snap deleted file mode 100644 index 611cc6ebb881d..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/__snapshots__/transformer.test.ts.snap +++ /dev/null @@ -1,183 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CPU chart data transformer should transform ES data 1`] = ` -Object { - "overallValues": Object { - "processCPUAverage": 200, - "processCPUMax": 100, - "systemCPUAverage": 400, - "systemCPUMax": 300, - }, - "series": Object { - "processCPUAverage": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 1, - "y": 20, - }, - Object { - "x": 2, - "y": 40, - }, - Object { - "x": 3, - "y": 60, - }, - Object { - "x": 4, - "y": 80, - }, - Object { - "x": 5, - "y": 100, - }, - Object { - "x": 6, - "y": 120, - }, - Object { - "x": 7, - "y": 140, - }, - Object { - "x": 8, - "y": 160, - }, - Object { - "x": 9, - "y": 180, - }, - ], - "processCPUMax": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 1, - "y": 10, - }, - Object { - "x": 2, - "y": 20, - }, - Object { - "x": 3, - "y": 30, - }, - Object { - "x": 4, - "y": 40, - }, - Object { - "x": 5, - "y": 50, - }, - Object { - "x": 6, - "y": 60, - }, - Object { - "x": 7, - "y": 70, - }, - Object { - "x": 8, - "y": 80, - }, - Object { - "x": 9, - "y": 90, - }, - ], - "systemCPUAverage": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 1, - "y": 40, - }, - Object { - "x": 2, - "y": 80, - }, - Object { - "x": 3, - "y": 120, - }, - Object { - "x": 4, - "y": 160, - }, - Object { - "x": 5, - "y": 200, - }, - Object { - "x": 6, - "y": 240, - }, - Object { - "x": 7, - "y": 280, - }, - Object { - "x": 8, - "y": 320, - }, - Object { - "x": 9, - "y": 360, - }, - ], - "systemCPUMax": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 1, - "y": 30, - }, - Object { - "x": 2, - "y": 60, - }, - Object { - "x": 3, - "y": 90, - }, - Object { - "x": 4, - "y": 120, - }, - Object { - "x": 5, - "y": 150, - }, - Object { - "x": 6, - "y": 180, - }, - Object { - "x": 7, - "y": 210, - }, - Object { - "x": 8, - "y": 240, - }, - Object { - "x": 9, - "y": 270, - }, - ], - }, - "totalHits": 199, -} -`; diff --git a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/fetcher.test.ts b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/fetcher.test.ts deleted file mode 100644 index e9a211ad09ee7..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/fetcher.test.ts +++ /dev/null @@ -1,16 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getSetupMock } from '../../../../testHelpers/mocks'; -import { fetch } from '../fetcher'; - -describe('CPU chart data fetcher', () => { - it('should fetch aggregations', async () => { - const mockSetup = getSetupMock(); - await fetch({ setup: mockSetup, serviceName: 'test-service' }); - expect(mockSetup.client.mock.calls).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/transformer.test.ts b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/transformer.test.ts deleted file mode 100644 index 388a5e09eb264..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/transformer.test.ts +++ /dev/null @@ -1,58 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ESResponse } from '../fetcher'; -import { transform } from '../transformer'; - -describe('CPU chart data transformer', () => { - it('should transform ES data', () => { - const response = { - aggregations: { - timeseriesData: { - buckets: Array(10) - .fill(1) - .map((_, i) => ({ - key: i, - systemCPUAverage: { value: i * 40 }, - systemCPUMax: { value: i * 30 }, - processCPUAverage: { value: i * 20 }, - processCPUMax: { value: i * 10 } - })) - }, - systemCPUAverage: { - value: 400 - }, - systemCPUMax: { - value: 300 - }, - processCPUAverage: { - value: 200 - }, - processCPUMax: { - value: 100 - } - }, - hits: { - total: 199 - } - } as ESResponse; - - const result = transform(response); - expect(result).toMatchSnapshot(); - - expect(result.series.systemCPUAverage).toHaveLength(10); - expect(result.series.systemCPUMax).toHaveLength(10); - expect(result.series.processCPUAverage).toHaveLength(10); - expect(result.series.processCPUMax).toHaveLength(10); - - expect(Object.keys(result.overallValues)).toEqual([ - 'systemCPUAverage', - 'systemCPUMax', - 'processCPUAverage', - 'processCPUMax' - ]); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/fetcher.ts deleted file mode 100644 index 9a6d07a0a6e4e..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/fetcher.ts +++ /dev/null @@ -1,81 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { ESFilter } from 'elasticsearch'; -import { - METRIC_PROCESS_CPU_PERCENT, - METRIC_SYSTEM_CPU_PERCENT, - PROCESSOR_EVENT, - SERVICE_NAME -} from '../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../typings/common'; -import { getBucketSize } from '../../helpers/get_bucket_size'; -import { AggValue, MetricsRequestArgs, TimeSeriesBucket } from '../query_types'; - -interface Bucket extends TimeSeriesBucket { - systemCPUAverage: AggValue; - systemCPUMax: AggValue; - processCPUAverage: AggValue; - processCPUMax: AggValue; -} - -interface Aggs { - timeseriesData: { - buckets: Bucket[]; - }; - systemCPUAverage: AggValue; - systemCPUMax: AggValue; - processCPUAverage: AggValue; - processCPUMax: AggValue; -} - -export type ESResponse = PromiseReturnType; -export async function fetch({ serviceName, setup }: MetricsRequestArgs) { - const { start, end, esFilterQuery, client, config } = setup; - const { bucketSize } = getBucketSize(start, end, 'auto'); - const filters: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, - { - range: { '@timestamp': { gte: start, lte: end, format: 'epoch_millis' } } - } - ]; - - if (esFilterQuery) { - filters.push(esFilterQuery); - } - - const params = { - index: config.get('apm_oss.metricsIndices'), - body: { - size: 0, - query: { bool: { filter: filters } }, - aggs: { - timeseriesData: { - date_histogram: { - field: '@timestamp', - - // ensure minimum bucket size of 30s since this is the default resolution for metric data - interval: `${Math.max(bucketSize, 30)}s`, - min_doc_count: 0, - extended_bounds: { min: start, max: end } - }, - aggs: { - systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } }, - systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } }, - processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } }, - processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } } - } - }, - systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } }, - systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } }, - processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } }, - processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } } - } - } - }; - - return client('search', params); -} diff --git a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/index.ts b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/index.ts deleted file mode 100644 index fb46d9afbfc44..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { MetricsRequestArgs } from '../query_types'; -import { fetch } from './fetcher'; -import { CPUChartAPIResponse, transform } from './transformer'; - -export { CPUChartAPIResponse }; - -export async function getCPUChartData(args: MetricsRequestArgs) { - const result = await fetch(args); - return transform(result); -} diff --git a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/transformer.ts b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/transformer.ts deleted file mode 100644 index 024ef76efa3cf..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/transformer.ts +++ /dev/null @@ -1,59 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { Coordinate } from '../../../../typings/timeseries'; -import { ESResponse } from './fetcher'; - -type CPUMetricName = - | 'systemCPUAverage' - | 'systemCPUMax' - | 'processCPUAverage' - | 'processCPUMax'; - -const CPU_METRIC_NAMES: CPUMetricName[] = [ - 'systemCPUAverage', - 'systemCPUMax', - 'processCPUAverage', - 'processCPUMax' -]; - -export type CPUChartAPIResponse = ReturnType; - -export function transform(result: ESResponse) { - const { aggregations, hits } = result; - const { - timeseriesData, - systemCPUAverage, - systemCPUMax, - processCPUAverage, - processCPUMax - } = aggregations; - - const series = { - systemCPUAverage: [] as Coordinate[], - systemCPUMax: [] as Coordinate[], - processCPUAverage: [] as Coordinate[], - processCPUMax: [] as Coordinate[] - }; - - // using forEach here to avoid looping over the entire dataset - // 4 times or doing a complicated, memory-heavy map/reduce - timeseriesData.buckets.forEach(({ key, ...bucket }) => { - CPU_METRIC_NAMES.forEach(name => { - series[name].push({ x: key, y: bucket[name].value }); - }); - }); - - return { - series, - overallValues: { - systemCPUAverage: systemCPUAverage.value, - systemCPUMax: systemCPUMax.value, - processCPUAverage: processCPUAverage.value, - processCPUMax: processCPUMax.value - }, - totalHits: hits.total - }; -} diff --git a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap deleted file mode 100644 index 064a4fa5d24b3..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap +++ /dev/null @@ -1,102 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`get memory chart data fetcher should fetch memory chart aggregations 1`] = ` -Array [ - Array [ - "search", - Object { - "body": Object { - "aggs": Object { - "memoryUsedAvg": Object { - "avg": Object { - "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", - }, - }, - }, - "memoryUsedMax": Object { - "max": Object { - "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", - }, - }, - }, - "timeseriesData": Object { - "aggs": Object { - "memoryUsedAvg": Object { - "avg": Object { - "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", - }, - }, - }, - "memoryUsedMax": Object { - "max": Object { - "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", - }, - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 200, - "min": 100, - }, - "field": "@timestamp", - "interval": "30s", - "min_doc_count": 0, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "service.name": "test-service", - }, - }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 100, - "lte": 200, - }, - }, - }, - Object { - "exists": Object { - "field": "system.memory.total", - }, - }, - Object { - "exists": Object { - "field": "system.memory.actual.free", - }, - }, - Object { - "term": Object { - "field": "test.esfilter.query", - }, - }, - ], - }, - }, - "size": 0, - }, - "index": undefined, - }, - ], -] -`; diff --git a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/__snapshots__/transformer.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/__snapshots__/transformer.test.ts.snap deleted file mode 100644 index a232612ff782b..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/__snapshots__/transformer.test.ts.snap +++ /dev/null @@ -1,97 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`memory chart data transformer should transform ES data 1`] = ` -Object { - "overallValues": Object { - "memoryUsedAvg": 300, - "memoryUsedMax": 400, - }, - "series": Object { - "memoryUsedAvg": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 1, - "y": 30, - }, - Object { - "x": 2, - "y": 60, - }, - Object { - "x": 3, - "y": 90, - }, - Object { - "x": 4, - "y": 120, - }, - Object { - "x": 5, - "y": 150, - }, - Object { - "x": 6, - "y": 180, - }, - Object { - "x": 7, - "y": 210, - }, - Object { - "x": 8, - "y": 240, - }, - Object { - "x": 9, - "y": 270, - }, - ], - "memoryUsedMax": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 1, - "y": 40, - }, - Object { - "x": 2, - "y": 80, - }, - Object { - "x": 3, - "y": 120, - }, - Object { - "x": 4, - "y": 160, - }, - Object { - "x": 5, - "y": 200, - }, - Object { - "x": 6, - "y": 240, - }, - Object { - "x": 7, - "y": 280, - }, - Object { - "x": 8, - "y": 320, - }, - Object { - "x": 9, - "y": 360, - }, - ], - }, - "totalHits": 199, -} -`; diff --git a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/fetcher.test.ts b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/fetcher.test.ts deleted file mode 100644 index c4c4298422bba..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/fetcher.test.ts +++ /dev/null @@ -1,16 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getSetupMock } from '../../../../testHelpers/mocks'; -import { fetch } from '../fetcher'; - -describe('get memory chart data fetcher', () => { - it('should fetch memory chart aggregations', async () => { - const mockSetup = getSetupMock(); - await fetch({ setup: mockSetup, serviceName: 'test-service' }); - expect(mockSetup.client.mock.calls).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/transformer.test.ts b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/transformer.test.ts deleted file mode 100644 index 8a7bbb1683057..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/transformer.test.ts +++ /dev/null @@ -1,42 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ESResponse } from '../fetcher'; -import { transform } from '../transformer'; - -describe('memory chart data transformer', () => { - it('should transform ES data', () => { - const response = { - aggregations: { - timeseriesData: { - buckets: Array(10) - .fill(1) - .map((_, i) => ({ - key: i, - memoryUsedMax: { value: i * 40 }, - memoryUsedAvg: { value: i * 30 } - })) - }, - memoryUsedMax: { value: 400 }, - memoryUsedAvg: { value: 300 } - }, - hits: { total: 199 } - } as ESResponse; - - const result = transform(response); - expect(result).toMatchSnapshot(); - - expect(result.series.memoryUsedMax).toHaveLength(10); - expect(result.series.memoryUsedAvg).toHaveLength(10); - - const overall = Object.keys(result.overallValues); - - expect(overall).toHaveLength(2); - expect(overall).toEqual( - expect.arrayContaining(['memoryUsedMax', 'memoryUsedAvg']) - ); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/fetcher.ts deleted file mode 100644 index 0bccff17aebe8..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/fetcher.ts +++ /dev/null @@ -1,80 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { ESFilter } from 'elasticsearch'; -import { - METRIC_SYSTEM_FREE_MEMORY, - METRIC_SYSTEM_TOTAL_MEMORY, - PROCESSOR_EVENT, - SERVICE_NAME -} from '../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../typings/common'; -import { getBucketSize } from '../../helpers/get_bucket_size'; -import { AggValue, MetricsRequestArgs, TimeSeriesBucket } from '../query_types'; - -interface Bucket extends TimeSeriesBucket { - memoryUsedAvg: AggValue; - memoryUsedMax: AggValue; -} - -interface Aggs { - timeseriesData: { - buckets: Bucket[]; - }; - memoryUsedAvg: AggValue; - memoryUsedMax: AggValue; -} - -export type ESResponse = PromiseReturnType; -export async function fetch({ serviceName, setup }: MetricsRequestArgs) { - const { start, end, esFilterQuery, client, config } = setup; - const { bucketSize } = getBucketSize(start, end, 'auto'); - const filters: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, - { - range: { '@timestamp': { gte: start, lte: end, format: 'epoch_millis' } } - }, - { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - { exists: { field: METRIC_SYSTEM_FREE_MEMORY } } - ]; - - if (esFilterQuery) { - filters.push(esFilterQuery); - } - - const script = { - lang: 'expression', - source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']` - }; - - const params = { - index: config.get('apm_oss.metricsIndices'), - body: { - size: 0, - query: { bool: { filter: filters } }, - aggs: { - timeseriesData: { - date_histogram: { - field: '@timestamp', - - // ensure minimum bucket size of 30s since this is the default resolution for metric data - interval: `${Math.max(bucketSize, 30)}s`, - min_doc_count: 0, - extended_bounds: { min: start, max: end } - }, - aggs: { - memoryUsedAvg: { avg: { script } }, - memoryUsedMax: { max: { script } } - } - }, - memoryUsedAvg: { avg: { script } }, - memoryUsedMax: { max: { script } } - } - } - }; - - return client('search', params); -} diff --git a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/index.ts b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/index.ts deleted file mode 100644 index 8b6dce6bf1c0e..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { MetricsRequestArgs } from '../query_types'; -import { fetch } from './fetcher'; -import { MemoryChartAPIResponse, transform } from './transformer'; - -export { MemoryChartAPIResponse }; - -export async function getMemoryChartData(args: MetricsRequestArgs) { - const result = await fetch(args); - return transform(result); -} diff --git a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/transformer.ts b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/transformer.ts deleted file mode 100644 index cc1ed0c9b2ce6..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/transformer.ts +++ /dev/null @@ -1,41 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { Coordinate } from '../../../../typings/timeseries'; -import { ESResponse } from './fetcher'; - -type MemoryMetricName = 'memoryUsedAvg' | 'memoryUsedMax'; -const MEMORY_METRIC_NAMES: MemoryMetricName[] = [ - 'memoryUsedAvg', - 'memoryUsedMax' -]; - -export type MemoryChartAPIResponse = ReturnType; -export function transform(result: ESResponse) { - const { aggregations, hits } = result; - const { timeseriesData, memoryUsedAvg, memoryUsedMax } = aggregations; - - const series = { - memoryUsedAvg: [] as Coordinate[], - memoryUsedMax: [] as Coordinate[] - }; - - // using forEach here to avoid looping over the entire dataset - // multiple times or doing a complicated, memory-heavy map/reduce - timeseriesData.buckets.forEach(({ key, ...bucket }) => { - MEMORY_METRIC_NAMES.forEach(name => { - series[name].push({ x: key, y: bucket[name].value }); - }); - }); - - return { - series, - overallValues: { - memoryUsedAvg: memoryUsedAvg.value, - memoryUsedMax: memoryUsedMax.value - }, - totalHits: hits.total - }; -} diff --git a/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts b/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts new file mode 100644 index 0000000000000..02b94c515d841 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Setup } from '../helpers/setup_request'; +import { getJavaMetricsCharts } from './by_agent/java'; +import { getDefaultMetricsCharts } from './by_agent/default'; +import { GenericMetricsChart } from './transform_metrics_chart'; + +export interface MetricsChartsByAgentAPIResponse { + charts: GenericMetricsChart[]; +} + +export async function getMetricsChartDataByAgent({ + setup, + serviceName, + agentName +}: { + setup: Setup; + serviceName: string; + agentName: string; +}): Promise { + switch (agentName) { + case 'java': { + return getJavaMetricsCharts(setup, serviceName); + } + + default: { + return getDefaultMetricsCharts(setup, serviceName); + } + } +} diff --git a/x-pack/plugins/apm/server/lib/metrics/query_types.ts b/x-pack/plugins/apm/server/lib/metrics/query_types.ts deleted file mode 100644 index 80def2dfece80..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/query_types.ts +++ /dev/null @@ -1,22 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Setup } from '../helpers/setup_request'; - -export interface MetricsRequestArgs { - serviceName: string; - setup: Setup; -} - -export interface AggValue { - value: number | null; -} - -export interface TimeSeriesBucket { - key_as_string: string; // timestamp as string - key: number; // timestamp as epoch milliseconds - doc_count: number; -} diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts new file mode 100644 index 0000000000000..e6fff34b37bc4 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AggregationSearchResponse } from 'elasticsearch'; +import { MetricsAggs, MetricSeriesKeys, AggValue } from './types'; +import { transformDataToMetricsChart } from './transform_metrics_chart'; +import { ChartType, YUnit } from '../../../typings/timeseries'; + +test('transformDataToMetricsChart should transform an ES result into a chart object', () => { + interface TestKeys extends MetricSeriesKeys { + a: AggValue; + b: AggValue; + c: AggValue; + } + + type R = AggregationSearchResponse>; + + const response = { + hits: { total: 5000 } as R['hits'], + aggregations: { + a: { value: 1000 }, + b: { value: 1000 }, + c: { value: 1000 }, + timeseriesData: { + buckets: [ + { + a: { value: 10 }, + b: { value: 10 }, + c: { value: 10 }, + key: 1 + } as R['aggregations']['timeseriesData']['buckets'][0], + { + a: { value: 20 }, + b: { value: 20 }, + c: { value: 20 }, + key: 2 + } as R['aggregations']['timeseriesData']['buckets'][0], + { + a: { value: 30 }, + b: { value: 30 }, + c: { value: 30 }, + key: 3 + } as R['aggregations']['timeseriesData']['buckets'][0] + ] + } + } as R['aggregations'] + } as R; + + const chartBase = { + title: 'Test Chart Title', + type: 'linemark' as ChartType, + key: 'test_chart_key', + yUnit: 'number' as YUnit, + series: { + a: { title: 'Series A', color: 'red' }, + b: { title: 'Series B', color: 'blue' }, + c: { title: 'Series C', color: 'green' } + } + }; + + const chart = transformDataToMetricsChart(response, chartBase); + + expect(chart).toMatchInlineSnapshot(` +Object { + "key": "test_chart_key", + "series": Array [ + Object { + "color": "red", + "data": Array [ + Object { + "x": 1, + "y": 10, + }, + Object { + "x": 2, + "y": 20, + }, + Object { + "x": 3, + "y": 30, + }, + ], + "key": "a", + "overallValue": 1000, + "title": "Series A", + "type": "linemark", + }, + Object { + "color": "blue", + "data": Array [ + Object { + "x": 1, + "y": 10, + }, + Object { + "x": 2, + "y": 20, + }, + Object { + "x": 3, + "y": 30, + }, + ], + "key": "b", + "overallValue": 1000, + "title": "Series B", + "type": "linemark", + }, + Object { + "color": "green", + "data": Array [ + Object { + "x": 1, + "y": 10, + }, + Object { + "x": 2, + "y": 20, + }, + Object { + "x": 3, + "y": 30, + }, + ], + "key": "c", + "overallValue": 1000, + "title": "Series C", + "type": "linemark", + }, + ], + "title": "Test Chart Title", + "totalHits": 5000, + "yUnit": "number", +} +`); +}); diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts new file mode 100644 index 0000000000000..9936b6883a1c7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AggregationSearchResponse } from 'elasticsearch'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { ChartBase, MetricsAggs, MetricSeriesKeys } from './types'; + +const colors = [ + theme.euiColorVis0, + theme.euiColorVis1, + theme.euiColorVis2, + theme.euiColorVis3, + theme.euiColorVis4, + theme.euiColorVis5, + theme.euiColorVis6 +]; + +export type GenericMetricsChart = ReturnType< + typeof transformDataToMetricsChart +>; +export function transformDataToMetricsChart( + result: AggregationSearchResponse>, + chartBase: ChartBase +) { + const { aggregations, hits } = result; + const { timeseriesData } = aggregations; + + return { + title: chartBase.title, + key: chartBase.key, + yUnit: chartBase.yUnit, + totalHits: hits.total, + series: Object.keys(chartBase.series).map((seriesKey, i) => ({ + title: chartBase.series[seriesKey].title, + key: seriesKey, + type: chartBase.type, + color: chartBase.series[seriesKey].color || colors[i], + overallValue: aggregations[seriesKey].value, + data: timeseriesData.buckets.map(bucket => { + const { value } = bucket[seriesKey]; + const y = value === null || isNaN(value) ? null : value; + return { + x: bucket.key, + y + }; + }) + })) + }; +} diff --git a/x-pack/plugins/apm/server/lib/metrics/types.ts b/x-pack/plugins/apm/server/lib/metrics/types.ts new file mode 100644 index 0000000000000..f234b44733444 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/types.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ChartType, YUnit } from '../../../typings/timeseries'; + +export interface AggValue { + value: number | null; +} + +export interface MetricSeriesKeys { + [key: string]: AggValue; +} + +export interface ChartBase { + title: string; + key: string; + type: ChartType; + yUnit: YUnit; + series: { + [key in keyof T]: { + title: string; + color?: string; + } + }; +} + +export type MetricsAggs = { + timeseriesData: { + buckets: Array< + { + key_as_string: string; // timestamp as string + key: number; // timestamp as epoch milliseconds + doc_count: number; + } & T + >; + }; +} & T; diff --git a/x-pack/plugins/apm/server/lib/services/get_service.ts b/x-pack/plugins/apm/server/lib/services/get_service.ts index f4a403f37264e..609763396f92f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service.ts @@ -65,7 +65,7 @@ export async function getService(serviceName: string, setup: Setup) { const { aggregations } = await client('search', params); const buckets = idx(aggregations, _ => _.types.buckets) || []; const types = buckets.map(bucket => bucket.key); - const agentName = idx(aggregations, _ => _.agents.buckets[0].key); + const agentName = idx(aggregations, _ => _.agents.buckets[0].key) || ''; return { serviceName, types, diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts index 997ff9b803c09..3bc82010aac77 100644 --- a/x-pack/plugins/apm/server/routes/metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics.ts @@ -5,10 +5,11 @@ */ import Boom from 'boom'; +import Joi from 'joi'; import { CoreSetup } from 'src/core/server'; import { withDefaultValidators } from '../lib/helpers/input_validation'; import { setupRequest } from '../lib/helpers/setup_request'; -import { getAllMetricsChartData } from '../lib/metrics/get_all_metrics_chart_data'; +import { getMetricsChartDataByAgent } from '../lib/metrics/get_metrics_chart_data_by_agent'; const defaultErrorHandler = (err: Error) => { // eslint-disable-next-line @@ -18,21 +19,27 @@ const defaultErrorHandler = (err: Error) => { export function initMetricsApi(core: CoreSetup) { const { server } = core.http; + server.route({ method: 'GET', path: `/api/apm/services/{serviceName}/metrics/charts`, options: { validate: { - query: withDefaultValidators() + query: withDefaultValidators({ + agentName: Joi.string().required() + }) }, tags: ['access:apm'] }, handler: async req => { const setup = setupRequest(req); const { serviceName } = req.params; - return await getAllMetricsChartData({ + // casting approach recommended here: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/25605 + const { agentName } = req.query as { agentName: string }; + return await getMetricsChartDataByAgent({ setup, - serviceName + serviceName, + agentName }).catch(defaultErrorHandler); } }); diff --git a/x-pack/plugins/apm/typings/timeseries.ts b/x-pack/plugins/apm/typings/timeseries.ts index 93d354b6bc1d4..1b10824234481 100644 --- a/x-pack/plugins/apm/typings/timeseries.ts +++ b/x-pack/plugins/apm/typings/timeseries.ts @@ -13,3 +13,6 @@ export interface RectCoordinate { x: number; x0: number; } + +export type ChartType = 'area' | 'linemark'; +export type YUnit = 'percent' | 'bytes' | 'number'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts index b67c49b82fe67..45c7d77ba29f1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts @@ -11,6 +11,7 @@ export const areaChart: ElementFactory = () => ({ name: 'areaChart', displayName: 'Area chart', help: 'A line chart with a filled body', + tags: ['chart'], image: header, expression: `filters | demodata diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts index 1a3a344b09a52..cb18e6ce27d66 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const bubbleChart: ElementFactory = () => ({ name: 'bubbleChart', displayName: 'Bubble chart', + tags: ['chart'], help: 'A customizable bubble chart', width: 700, height: 300, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/debug/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/debug/index.ts index 289d7020a727f..2a68cfd215bcb 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/debug/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/debug/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const debug: ElementFactory = () => ({ name: 'debug', displayName: 'Debug', + tags: ['text'], help: 'Just dumps the configuration of the element', image: header, expression: `demodata diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/donut/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/donut/index.ts index 1b79c6376b48f..c7edda00b21f9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/donut/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/donut/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const donut: ElementFactory = () => ({ name: 'donut', displayName: 'Donut chart', + tags: ['chart', 'proportion'], help: 'A customizable donut chart', image: header, expression: `filters diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts index 7e90295e5f623..9f076a3652be7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const dropdownFilter: ElementFactory = () => ({ name: 'dropdown_filter', displayName: 'Dropdown filter', + tags: ['filter'], help: 'A dropdown from which you can select values for an "exactly" filter', image: header, height: 50, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts index aa0a168b39c4e..2cee7fd138fd2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const horizontalBarChart: ElementFactory = () => ({ name: 'horizontalBarChart', displayName: 'Horizontal bar chart', + tags: ['chart'], help: 'A customizable horizontal bar chart', image: header, expression: `filters diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts index 38c1468796523..8ef7eee562571 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts @@ -11,6 +11,7 @@ import header from './header.png'; export const horizontalProgressBar: ElementFactory = () => ({ name: 'horizontalProgressBar', displayName: 'Horizontal progress bar', + tags: ['chart', 'proportion'], help: 'Displays progress as a portion of a horizontal bar', width: 400, height: 30, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts index 6e6a276afbe69..1533092a16017 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts @@ -11,6 +11,7 @@ import header from './header.png'; export const horizontalProgressPill: ElementFactory = () => ({ name: 'horizontalProgressPill', displayName: 'Horizontal progress pill', + tags: ['chart', 'proportion'], help: 'Displays progress as a portion of a horizontal pill', width: 400, height: 30, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/image/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/image/index.ts index edc074fd627da..8a7d9dfa7e957 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/image/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/image/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const image: ElementFactory = () => ({ name: 'image', displayName: 'Image', + tags: ['graphic'], help: 'A static image', image: header, expression: `image dataurl=null mode="contain" diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts index b27f9988b8295..1992dcd60814f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const lineChart: ElementFactory = () => ({ name: 'lineChart', displayName: 'Line chart', + tags: ['chart'], help: 'A customizable line chart', image: header, expression: `filters diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts index 0264845ee7ee8..7bcb0c9d62c2b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts @@ -10,6 +10,7 @@ import { ElementFactory } from '../types'; export const markdown: ElementFactory = () => ({ name: 'markdown', displayName: 'Markdown', + tags: ['text'], help: 'Markup from Markdown', image: header, expression: `filters diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index 9af362fadbf74..6f1e5cdf84685 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -11,6 +11,7 @@ import { ElementFactory } from '../types'; export const metric: ElementFactory = () => ({ name: 'metric', displayName: 'Metric', + tags: ['text'], help: 'A number with a label', width: 200, height: 100, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts index 4c69e7b3af6a6..0e7aed5a86194 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts @@ -10,6 +10,7 @@ import { ElementFactory } from '../types'; export const pie: ElementFactory = () => ({ name: 'pie', displayName: 'Pie chart', + tags: ['chart', 'proportion'], width: 300, height: 300, help: 'A simple pie chart', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts index 66109c57104b8..a4d12b26a3b54 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const plot: ElementFactory = () => ({ name: 'plot', displayName: 'Coordinate plot', + tags: ['chart'], help: 'Mixed line, bar or dot charts', image: header, expression: `filters diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts index 150eafa38a857..d55c6410885b6 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts @@ -11,6 +11,7 @@ import header from './header.png'; export const progressGauge: ElementFactory = () => ({ name: 'progressGauge', displayName: 'Progress gauge', + tags: ['chart', 'proportion'], help: 'Displays progress as a portion of a gauge', width: 200, height: 200, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts index 3df62f55e1e39..9324e9c2f5165 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts @@ -11,6 +11,7 @@ import header from './header.png'; export const progressSemicircle: ElementFactory = () => ({ name: 'progressSemicircle', displayName: 'Progress semicircle', + tags: ['chart', 'proportion'], help: 'Displays progress as a portion of a semicircle', width: 200, height: 100, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts index abdaa44eb42fd..a047a8fc9c241 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts @@ -11,6 +11,7 @@ import header from './header.png'; export const progressWheel: ElementFactory = () => ({ name: 'progressWheel', displayName: 'Progress wheel', + tags: ['chart', 'proportion'], help: 'Displays progress as a portion of a wheel', width: 200, height: 200, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts index 9b6d826ad5f61..4a72f9dfb7139 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const repeatImage: ElementFactory = () => ({ name: 'repeatImage', displayName: 'Image repeat', + tags: ['graphic', 'proportion'], help: 'Repeats an image N times', image: header, expression: `filters diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts index 92da9c8025a0d..30c9799308b58 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const revealImage: ElementFactory = () => ({ name: 'revealImage', displayName: 'Image reveal', + tags: ['graphic', 'proportion'], help: 'Reveals a percentage of an image', image: header, expression: `filters diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/shape/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/shape/index.ts index 673f97d42b67b..84372a992720a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/shape/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/shape/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const shape: ElementFactory = () => ({ name: 'shape', displayName: 'Shape', + tags: ['graphic'], help: 'A customizable shape', width: 200, height: 200, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts index 30cfac2c0516b..217ddd81e0ba1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const table: ElementFactory = () => ({ name: 'table', displayName: 'Data table', + tags: ['text'], help: 'A scrollable grid for displaying data in a tabular format', image: header, expression: `filters diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts index 4611ddb873c42..ae48f2721cba3 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const tiltedPie: ElementFactory = () => ({ name: 'tiltedPie', displayName: 'Tilted pie chart', + tags: ['chart', 'proportion'], width: 500, height: 250, help: 'A customizable tilted pie chart', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts index f0d5eb618da21..cb107cc86aa02 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const timeFilter: ElementFactory = () => ({ name: 'time_filter', displayName: 'Time filter', + tags: ['filter'], help: 'Set a time window', image: header, height: 50, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/types.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/types.ts index 528198ddfc341..e122505e0380f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/types.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/types.ts @@ -9,6 +9,7 @@ export interface ElementSpec { image: string; expression: string; displayName?: string; + tags?: string[]; help?: string; filter?: string; width?: number; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts index 34f42398a2715..b0567b53bd874 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts @@ -10,6 +10,7 @@ import header from './header.png'; export const verticalBarChart: ElementFactory = () => ({ name: 'verticalBarChart', displayName: 'Vertical bar chart', + tags: ['chart'], help: 'A customizable vertical bar chart', image: header, expression: `filters diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts index 1d963b70bf52d..b7ba3fb9b2b2c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts @@ -11,6 +11,7 @@ import header from './header.png'; export const verticalProgressBar: ElementFactory = () => ({ name: 'verticalProgressBar', displayName: 'Vertical progress bar', + tags: ['chart', 'proportion'], help: 'Displays progress as a portion of a vertical bar', width: 80, height: 400, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts index 4e412f5c7edfc..61a02447de926 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts @@ -11,6 +11,7 @@ import header from './header.png'; export const verticalProgressPill: ElementFactory = () => ({ name: 'verticalProgressPill', displayName: 'Vertical progress pill', + tags: ['chart', 'proportion'], help: 'Displays progress as a portion of a vertical pill', width: 80, height: 400, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts new file mode 100644 index 0000000000000..418d1d11600a4 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TagFactory } from '../../../public/lib/tag'; + +export const chart: TagFactory = () => ({ name: 'chart', color: '#FEB6DB' }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts new file mode 100644 index 0000000000000..b65a3ddb5595c --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TagFactory } from '../../../public/lib/tag'; + +export const filter: TagFactory = () => ({ name: 'filter', color: '#3185FC' }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts new file mode 100644 index 0000000000000..fc7e18f45ace8 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TagFactory } from '../../../public/lib/tag'; + +export const graphic: TagFactory = () => ({ name: 'graphic', color: '#E6C220' }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/index.ts similarity index 58% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/tags/index.js rename to x-pack/plugins/canvas/canvas_plugin_src/uis/tags/index.ts index 2e711437c72a8..2587665a452b5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/index.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { chart } from './chart'; +import { filter } from './filter'; +import { graphic } from './graphic'; import { presentation } from './presentation'; +import { proportion } from './proportion'; import { report } from './report'; +import { text } from './text'; // Registry expects a function that returns a spec object -export const tagSpecs = [presentation, report]; +export const tagSpecs = [chart, filter, graphic, presentation, proportion, report, text]; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/register_lifecycle_routes.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/presentation.ts similarity index 62% rename from x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/register_lifecycle_routes.js rename to x-pack/plugins/canvas/canvas_plugin_src/uis/tags/presentation.ts index ba179d14b8112..6417a293838d7 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/register_lifecycle_routes.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/presentation.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerCreateRoute } from './register_create_route'; +import { TagFactory } from '../../../public/lib/tag'; -export function registerLifecycleRoutes(server) { - registerCreateRoute(server); -} +export const presentation: TagFactory = () => ({ name: 'presentation', color: '#017D73' }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/presentation.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js similarity index 75% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/tags/presentation.js rename to x-pack/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js index 80c27e6bfdfa6..ac6447ffd9dc0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/presentation.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const presentation = () => ({ name: 'presentation', color: '#1EA593' }); +export const proportion = () => ({ name: 'proportion', color: '#490092' }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts new file mode 100644 index 0000000000000..7b9926d91bc53 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TagFactory } from '../../../public/lib/tag'; + +export const proportion: TagFactory = () => ({ name: 'proportion', color: '#490092' }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/report.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/report.ts new file mode 100644 index 0000000000000..d79504e81bf42 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/report.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TagFactory } from '../../../public/lib/tag'; + +export const report: TagFactory = () => ({ name: 'report', color: '#DB1374' }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/text.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/text.ts new file mode 100644 index 0000000000000..8bedc2fb49344 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/tags/text.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TagFactory } from '../../../public/lib/tag'; + +export const text: TagFactory = () => ({ name: 'text', color: '#D3DAE6' }); diff --git a/x-pack/plugins/canvas/common/lib/dataurl.ts b/x-pack/plugins/canvas/common/lib/dataurl.ts index 4542a6a9fccd7..eddc8da1a51aa 100644 --- a/x-pack/plugins/canvas/common/lib/dataurl.ts +++ b/x-pack/plugins/canvas/common/lib/dataurl.ts @@ -42,7 +42,10 @@ export function parseDataUrl(str: string, withData = false) { }; } -export function isValidDataUrl(str: string) { +export function isValidDataUrl(str?: string) { + if (!str) { + return false; + } return dataurlRegex.test(str); } diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx new file mode 100644 index 0000000000000..db1427d6a7201 --- /dev/null +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, cleanup } from 'react-testing-library'; +import { + withUnconnectedElementsLoadedTelemetry, + WorkpadLoadedMetric, + WorkpadLoadedWithErrorsMetric, +} from '../workpad_telemetry'; + +const trackMetric = jest.fn(); +const Component = withUnconnectedElementsLoadedTelemetry(() =>
, trackMetric); + +const mockWorkpad = { + id: 'workpadid', + pages: [ + { + elements: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], + }, + { + elements: [{ id: '5' }], + }, + ], +}; + +const resolvedArgsMatchWorkpad = { + '1': {}, + '2': {}, + '3': {}, + '4': {}, + '5': {}, +}; + +const resolvedArgsNotMatchWorkpad = { + 'non-matching-id': {}, +}; + +const pendingCounts = { + pending: 5, + error: 0, + ready: 0, +}; + +const readyCounts = { + pending: 0, + error: 0, + ready: 5, +}; + +const errorCounts = { + pending: 0, + error: 1, + ready: 4, +}; + +describe('Elements Loaded Telemetry', () => { + beforeEach(() => { + trackMetric.mockReset(); + }); + + afterEach(cleanup); + + it('tracks when all resolvedArgs are completed', () => { + const { rerender } = render( + + ); + + expect(trackMetric).not.toBeCalled(); + + rerender( + + ); + + expect(trackMetric).toBeCalledWith(WorkpadLoadedMetric); + }); + + it('only tracks loaded once', () => { + const { rerender } = render( + + ); + + expect(trackMetric).not.toBeCalled(); + + rerender( + + ); + rerender( + + ); + + expect(trackMetric).toBeCalledTimes(1); + }); + + it('does not track if resolvedArgs are never pending', () => { + const { rerender } = render( + + ); + + rerender( + + ); + + expect(trackMetric).not.toBeCalled(); + }); + + it('tracks if elements are in error state after load', () => { + const { rerender } = render( + + ); + + expect(trackMetric).not.toBeCalled(); + + rerender( + + ); + + expect(trackMetric).toBeCalledWith([WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]); + }); + + it('tracks when the workpad changes and is loaded', () => { + const otherWorkpad = { + id: 'otherworkpad', + pages: [ + { + elements: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], + }, + { + elements: [{ id: '5' }], + }, + ], + }; + + const { rerender } = render( + + ); + + expect(trackMetric).not.toBeCalled(); + + rerender( + + ); + + expect(trackMetric).not.toBeCalled(); + + rerender( + + ); + + expect(trackMetric).toBeCalledWith(WorkpadLoadedMetric); + }); + + it('does not track if workpad has no elements', () => { + const otherWorkpad = { + id: 'otherworkpad', + pages: [], + }; + + const resolvedArgs = {}; + + const { rerender } = render( + + ); + + rerender( + + ); + + expect(trackMetric).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js index 3bd545bead8fe..aa19ae1c2b55b 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js @@ -11,6 +11,7 @@ import { canUserWrite, getAppReady } from '../../../state/selectors/app'; import { getWorkpad, isWriteable } from '../../../state/selectors/workpad'; import { LoadWorkpad } from './load_workpad'; import { WorkpadApp as Component } from './workpad_app'; +import { withElementsLoadedTelemetry } from './workpad_telemetry'; const mapStateToProps = state => { const appReady = getAppReady(state); @@ -36,5 +37,6 @@ export const WorkpadApp = compose( mapStateToProps, mapDispatchToProps ), - ...branches + ...branches, + withElementsLoadedTelemetry )(Component); diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx new file mode 100644 index 0000000000000..0727f7a528420 --- /dev/null +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +// @ts-ignore: Local Untyped +import { trackCanvasUiMetric } from '../../../lib/ui_metric'; +// @ts-ignore: Local Untyped +import { getElementCounts } from '../../../state/selectors/workpad'; +// @ts-ignore: Local Untyped +import { getArgs } from '../../../state/selectors/resolved_args'; + +const WorkpadLoadedMetric = 'workpad-loaded'; +const WorkpadLoadedWithErrorsMetric = 'workpad-loaded-with-errors'; + +export { WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric }; + +const mapStateToProps = (state: any) => ({ + telemetryElementCounts: getElementCounts(state), + telemetryResolvedArgs: getArgs(state), +}); + +/** + Counts of the loading states of workpad elements +*/ +interface ElementCounts { + /** Count of elements in error state */ + error: number; + /** Count of elements in pending state */ + pending: number; + /** Count of elements in ready state */ + ready: number; +} + +// TODO: Build out full workpad types +/** + Individual Page of a Workpad + */ +interface WorkpadPage { + /** The elements on this Workpad Page */ + elements: Array<{ id: string }>; +} + +/** + A canvas workpad object + */ +interface Workpad { + /** The pages of the workpad */ + pages: WorkpadPage[]; + /** The ID of the workpad */ + id: string; +} + +/** + Collection of resolved elements + */ +interface ResolvedArgs { + [keys: string]: any; +} + +interface ElementsLoadedTelemetryProps { + telemetryElementCounts: ElementCounts; + workpad: Workpad; + telemetryResolvedArgs: {}; +} + +function areAllElementsInResolvedArgs(workpad: Workpad, resolvedArgs: ResolvedArgs): boolean { + const resolvedArgsElements = Object.keys(resolvedArgs); + + const workpadElements = workpad.pages.reduce((reduction, page) => { + return [...reduction, ...page.elements.map(element => element.id)]; + }, []); + + return workpadElements.every(element => resolvedArgsElements.includes(element)); +} + +export const withUnconnectedElementsLoadedTelemetry = function

( + Component: React.ComponentType

, + trackMetric: (metric: string | string[]) => void = trackCanvasUiMetric +): React.SFC

{ + return function ElementsLoadedTelemetry( + props: P & ElementsLoadedTelemetryProps + ): React.SFCElement

{ + const { telemetryElementCounts, workpad, telemetryResolvedArgs, ...other } = props; + + const [currentWorkpadId, setWorkpadId] = useState(undefined); + const [hasReported, setHasReported] = useState(false); + + useEffect(() => { + const resolvedArgsAreForWorkpad = areAllElementsInResolvedArgs( + workpad, + telemetryResolvedArgs + ); + + if (workpad.id !== currentWorkpadId) { + setWorkpadId(workpad.id); + + const workpadElementCount = workpad.pages.reduce( + (reduction, page) => reduction + page.elements.length, + 0 + ); + + if ( + workpadElementCount === 0 || + (resolvedArgsAreForWorkpad && telemetryElementCounts.pending === 0) + ) { + setHasReported(true); + } else { + setHasReported(false); + } + } else if ( + !hasReported && + telemetryElementCounts.pending === 0 && + resolvedArgsAreForWorkpad + ) { + if (telemetryElementCounts.error > 0) { + trackMetric([WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]); + } else { + trackMetric(WorkpadLoadedMetric); + } + + setHasReported(true); + } + }); + + return ; + }; +}; + +export const withElementsLoadedTelemetry =

( + Component: React.ComponentType

+) => { + const telemetry = withUnconnectedElementsLoadedTelemetry(Component); + return connect(mapStateToProps)(telemetry); +}; diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot new file mode 100644 index 0000000000000..4fac77ebf47e2 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot @@ -0,0 +1,1405 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/CustomElementModal with description 1`] = ` +Array [ +

, +
, +
+
+ +
+
+
+

+ Edit custom element +

+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+ 40 characters remaining +
+
+
+
+ +
+